CVE-2020-13702: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: The Rolling Proximity Identifier used in the Apple/Google Exposure Notification API beta through 2020-05-29 enables attackers to circumvent Bluetooth Smart Privacy because there is a secondary temporary UID. An attacker with access to Beacon or IoT networks can seamlessly track individual device movement via a Bluetooth LE discovery mechanism.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 10
Published: 2020-06-11T19:15Z
Repository: https://github.com/google/exposure-notifications-internals
Language: Java
Files changed: .github/workflows/android.yml, .gitignore, .idea/codeStyles/Project.xml, .idea/codeStyles/codeStyleConfig.xml, .idea/copyright/profiles_settings.xml, .idea/vcs.xml, BUILD.md, CONTRIBUTING.md, CRYPTO.md, LICENSE, README.md, app/.gitignore, app/build.gradle, app/proguard-rules.pro, app/src/androidTest/java/com/google/samples/ensnippets/RoundTripTests.kt, app/src/androidTest/java/com/google/samples/ensnippets/SignatureVerificationTests.kt, app/src/androidTest/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGeneratorTest.java, app/src/androidTest/java/com/google/samples/exposurenotification/testing/AssociatedEncryptedMetadataGeneratorTest.java, app/src/androidTest/java/com/google/samples/exposurenotification/testing/TestVectors.java, app/src/main/AndroidManifest.xml, app/src/main/cpp/CMakeLists.txt, app/src/main/cpp/native-lib.cpp, app/src/main/java/com/google/samples/ensnippets/MainActivity.kt, app/src/main/res/drawable-v24/ic_launcher_foreground.xml, app/src/main/res/drawable/ic_launcher_background.xml, app/src/main/res/layout/activity_main.xml, app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml, app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml, app/src/main/res/values/colors.xml, app/src/main/res/values/strings.xml, app/src/main/res/values/styles.xml, build.gradle, en-risks-and-mitigations-faq.md, exposurenotification/.gitignore, exposurenotification/build.gradle, exposurenotification/proguard-rules.pro, exposurenotification/src/main/AndroidManifest.xml, exposurenotification/src/main/cpp/CMakeLists.txt, exposurenotification/src/main/cpp/constants.h, exposurenotification/src/main/cpp/key_file_parser.cc, exposurenotification/src/main/cpp/key_file_parser.h, exposurenotification/src/main/cpp/matching_helper.cc, exposurenotification/src/main/cpp/matching_helper.h, exposurenotification/src/main/cpp/matchingjni.cc, exposurenotification/src/main/cpp/nanopb_encoder.cc, exposurenotification/src/main/cpp/nanopb_encoder.h, exposurenotification/src/main/cpp/prefix_id_map.cc, exposurenotification/src/main/cpp/prefix_id_map.h, exposurenotification/src/main/java/com/google/samples/ByteArrayValue.java, exposurenotification/src/main/java/com/google/samples/Clock.java, exposurenotification/src/main/java/com/google/samples/Hex.java, exposurenotification/src/main/java/com/google/samples/PlatformVersion.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/Log.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/TekMetadataUtils.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/annotations/Hide.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertisementGenerator.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertiser.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/RollingProximityIdManager.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/data/RollingProximityIdValidator.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/data/TimeIntervalNumberUtility.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleDatabaseWriter.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleExecutors.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleKeyGenerator.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleScanner.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/scanner/BleScannerImpl.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/utils/BluetoothUtils.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/utils/Constants.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/AesCtrEncryptor.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/AesEcbEncryptor.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/CryptoException.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/KeyDerivation.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/AssociatedEncryptedMetadata.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/BluetoothMetadata.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/DayNumber.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/GeneratedRollingProximityId.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/RollingProximityId.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/TemporaryExposureKeySupport.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/fileformat/StreamingTemporaryExposureKeyFileParser.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/fileformat/TemporaryExposureKeyConverter.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataGenerator.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataHelper.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGenerator.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGeneratorBase.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/TemporaryExposureKeyGenerator.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/features/ContactTracingFeature.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ContactRecordLookUpTable.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/DailySummaryUtils.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ExposureMatchingTracer.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ExposureWindowUtils.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/KeyExposureEvaluator.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingJni.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingRequest.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/Period.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ProvideDiagnosisKeys.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/RiskScoreCalculator.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/SightingRecordWithMetadata.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/SignatureVerifier.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TemporaryExposureKeyExportV1Header.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TemporaryExposureKeyFileMetadataParser.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TimeAndAttenuation.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TracingParams.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/AdvertisementPacket.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/CalibrationConfidence.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DailySummariesConfig.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DailySummary.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DiagnosisKeyFileProvider.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DiagnosisKeysDataMapping.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureConfiguration.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureInformation.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationClient.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationStatus.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationStatusCodes.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureSummary.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureWindow.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/Infectiousness.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/PackageConfiguration.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ReportType.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/RiskLevel.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ScanInstance.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ScannedPacket.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/TemporaryExposureKey.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/WakeUpService.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/AbstractSafeParcelable.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/ReflectedParcelable.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/SafeParcelable.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/CloseableIterable.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/CloseableIterables.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ContactRecordDataStore.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/Encoder.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/EncodingComparisonHelper.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ExposureConfigurationConverter.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ExposureResultStorage.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/SelfTemporaryExposureKeyDataStore.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/StorageException.java, exposurenotification/src/main/java/com/google/samples/exposurenotification/threads/TracingHandler.java, exposurenotification/src/main/proto/api_call_record.proto, exposurenotification/src/main/proto/client_record.proto, exposurenotification/src/main/proto/contact_record.proto, exposurenotification/src/main/proto/exposure_key_export.proto, exposurenotification/src/main/proto/exposure_notification_enums.proto, exposurenotification/src/main/proto/exposure_result.proto, exposurenotification/src/main/proto/matching_request.proto, exposurenotification/src/main/proto/temporary_exposure_key_properties.proto, exposurenotification/src/main/proto/tracing_params_record.proto, gradle.properties, gradle/wrapper/gradle-wrapper.properties, gradlew, gradlew.bat, settings.gradle, third_party/boringssl/.dockerignore, third_party/boringssl/.gitignore, third_party/boringssl/Dockerfile, third_party/boringssl/build-android.sh, third_party/nanopb/.gitignore, third_party/nanopb/download.sh
Commit date: 2020-10-01 17:09:04 -0700
Commit message: Update en-risks-and-mitigations-faq.md
Diff: diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
new file mode 100644
index 0000000..dfe10b5
--- /dev/null
+++ b/.github/workflows/android.yml
@@ -0,0 +1,31 @@
+name: Android CI
+
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+    branches: [ main ]
+
+jobs:
+
+  test:
+    name: Run Unit Tests
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v1
+      - name: Set up JDK 9
+        uses: actions/setup-java@v1
+        with:
+          java-version: 9
+
+      - name: Install dependencies
+        run: |
+          bash ./third_party/nanopb/download.sh
+          bash ./third_party/boringssl/build-android.sh
+
+      - name: Gradle build
+        run: bash ./gradlew -Pci --no-parallel build --stacktrace
+
+      - name: Gradle test
+        run: bash ./gradlew -Pci --no-parallel test --stacktrace
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7c5c2a7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+.classpath
+.gradle
+**/.idea/**
+!**/.idea/codeStyleSettings.xml
+!**/.idea/copyright
+!**/.idea/codeStyles
+!.idea/codeStyles/Project.xml
+!.idea/codeStyles/codeStyleConfig.xml
+!.idea/copyright/AndroidCopyright.xml
+!.idea/copyright/profiles_settings.xml
+!.idea/inspectionProfiles/Project_Default.xml
+!.idea/vcs.xml
+.project
+.settings/
+project.properties
+local.properties
+**/bin
+**/gen
+*.iml
+**/out
+**/build
+**/.cxx
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 0000000..87d084d
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,152 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <JavaCodeStyleSettings>
+      <option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
+    </JavaCodeStyleSettings>
+    <JetCodeStyleSettings>
+      <option name="PACKAGES_TO_USE_STAR_IMPORTS">
+        <value />
+      </option>
+      <option name="PACKAGES_IMPORT_LAYOUT">
+        <value>
+          <package name="" alias="false" withSubpackages="true" />
+          <package name="java" alias="false" withSubpackages="true" />
+          <package name="javax" alias="false" withSubpackages="true" />
+          <package name="kotlin" alias="false" withSubpackages="true" />
+          <package name="" alias="true" withSubpackages="true" />
+        </value>
+      </option>
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
+      <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </JetCodeStyleSettings>
+    <codeStyleSettings language="JAVA">
+      <option name="WRAP_ON_TYPING" value="1" />
+    </codeStyleSettings>
+    <codeStyleSettings language="Markdown">
+      <option name="WRAP_ON_TYPING" value="1" />
+    </codeStyleSettings>
+    <codeStyleSettings language="ObjectiveC">
+      <indentOptions>
+        <option name="INDENT_SIZE" value="2" />
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+        <option name="TAB_SIZE" value="2" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="XML">
+      <indentOptions>
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      </indentOptions>
+      <arrangement>
+        <rules>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:android</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:id</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>style</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>ANDROID_ATTRIBUTE_ORDER</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>.*</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+        </rules>
+      </arrangement>
+    </codeStyleSettings>
+    <codeStyleSettings language="kotlin">
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </codeStyleSettings>
+  </code_scheme>
+</component>
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..79ee123
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>
\ No newline at end of file
diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..008d780
--- /dev/null
+++ b/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,7 @@
+<component name="CopyrightManager">
+  <settings>
+    <module2copyright>
+      <element module="All" copyright="Google" />
+    </module2copyright>
+  </settings>
+</component>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/BUILD.md b/BUILD.md
new file mode 100644
index 0000000..4f6e4c1
--- /dev/null
+++ b/BUILD.md
@@ -0,0 +1,63 @@
+# Building
+
+This source in this project does not create a usable product, but is provided as a reference that
+can be used with the following specifications:
+
+- [Bluetooth Specification](https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf)
+- [Cryptography Specification](https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf)
+
+It is nonetheless possible to compile this code in order to run the various test cases provided.
+
+## Dependencies
+
+This project requires Java 9 to build the `exposurenotification` subproject. You can check the Java
+version by running `java -version`. If you want to be able to build from Android Studio, you can
+change the JDK location under `File > Project Structure > JDK Location`.
+
+This also project depends on a few third-party, open source libraries:
+
+* [BoringSSL](https://boringssl.googlesource.com/boringssl/)
+* [NaoPB](https://jpa.kapsi.fi/nanopb/download/)
+
+It is helpful to place both of these dependencies in a common directory. For convenience, shell
+scripts which download and build these third-party libraries are provided which require Docker to
+be installed to run:
+
+* [Docker Engine](https://docs.docker.com/engine/install/)
+
+## Building BoringSSL
+
+### Using Docker
+
+To build cross-compile BoringSSL for Android using Docker, run the following bash script:
+```bash
+bash ./third_party/boringssl/build-android.sh
+```
+
+### Using Native Toolchain
+
+To build BoringSSL, clone the repo and build it according to the steps described in the included
+`BUILDING.md` file.
+
+NOTE: This project is set to build for `arm64-v8a` and `x86`, in order to support testing on
+modern Android devices, as well as emulators. As such, it expects the output from building
+BoringSSL to be in a directory named `build-${ANDROID_ABI}`. This means BoringSSL must be built
+twice. Once with the setting `-DANDROID_ABI=x86`, for Android emulators, and a second time with
+ `-DANDROID_ABI=arm64-v8a` for physical devices.
+
+Depending on where you place the resulting build artifacts, you may need to change the
+`boringSslRoot` variable in the `build.gradle` file.
+
+## Downloading NanoPB
+
+The `build.gradle` file expects a binary distribution of NanoPB to be present under the folder
+`./third_party/nanopb/${platform}`. You can run the provided bash script to download it:
+```bash
+bash ./third_party/nanopb/download.sh
+```
+
+Alternatively, you can manually download a binary distribution of NanoPB from the provided link
+above. Extract it under the folder described above, or configure `build.gradle` accordingly.
+
+
+At this point the project should be able to build and the tests should run.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..bb70059
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution,
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google.com/conduct/).
diff --git a/CRYPTO.md b/CRYPTO.md
new file mode 100644
index 0000000..46eb228
--- /dev/null
+++ b/CRYPTO.md
@@ -0,0 +1,237 @@
+# Exposure Notifications Cryptography
+
+This document contains snippets of code from the sample reference implementation, occasionally in pseudocode, for the
+Implementation of the [Exposure Notification Cryptography
+Specification](https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf).
+
+Many of the snippets included in this document have been simplified from their actual
+implementation. Constants, select parameters, and member variables have been inlined for brevity.
+The actual implementation can be seen by inspecting the classes referenced.
+
+Because this document follows the ordering of the specification, we recommend you read them
+together.
+
+## Cryptographic Overview
+
+The following chart  may be helpful for better understanding how the various parts of the system
+fit together.
+
+<img src="images/Crypto-Flowchart.png">
+
+## ENIntervalNumber
+
+This function provides a number for each 10 minute time window that’s shared between all devices
+participating in the protocol. These time windows are derived from timestamps in Unix Epoch Time.
+
+```java
+private static int getCurrentEnIntervalNumber(Instant instant) {
+    return (int) (instant.toEpochMilli() / MINUTES.toMillis(10));
+}
+```
+
+`ENIntervalNumber` is part of
+`com.google.samples.exposurenotification.ble.advertising.RollingProximityIdManager`.
+
+## TEKRollingPeriod
+
+The TEKRollingPeriod is the duration for which a Temporary Exposure Key is valid (in multiples of 10
+minutes). In our current implementation, the TEKRollingPeriod is  144, achieving a key validity of 24 hours. 
+
+The rolling period is part of the 
+`com.google.samples.exposurenotification.nearby.TemporaryExposureKey` class and is set by the
+`TemporaryExposureKey.Builder#setRollingPeriod` method.
+
+## Temporary Exposure Key
+
+Temporary Exposure Keys (TEK) are not created explicitly, but rather implicitly as required. This happens due to one of two reasons:
+
+* This is the very first TEK being generated
+* The current TEK has expired
+
+This check is performed by first calling `RollingProximityIdManager#getExistingKey`. If this call returns `null`, then a new key is generated.
+
+If a TEK is returned, the code compares the current rolling period with the last valid rolling period for the key. If that check indicates the key has expired (the current period is greater than the last rolling period), a new TEK is generated.
+
+Pseudocode of for this check follows:
+
+```java
+var temporaryExposureKey = getCurrentTEK();
+if (temporaryExposureKey == null ||
+    currentEnIntervalNumber > temporaryExposureKey.getlastValidRollingPeriod()) {
+    temporaryExposureKey = TemporaryExposureKeyGenerator.generateKey();
+}
+```
+
+The implementation of of `getCurrentTEK()` is provided by `RollingProximityIdManager#getExistingKey` and the value of `currentEnIntervalNumber` is calculated by the function `RollingProximityIdManager#getCurrentEnIntervalNumber`.
+
+The key data is generated from a cryptographic random number generator so there is no link between TEKs. The method is in
+`com.google.samples.exposurenotification.data.generator.TemporaryExposureKeyGenerator`
+
+```java
+public static TemporaryExposureKey generateKey(
+        int rollingStartIntervalNumber, int rollingPeriod) {
+    SecureRandom random = new SecureRandom();
+    byte[] key = new byte[16];
+    random.nextBytes(key);
+    // ...
+}
+```
+
+## Rolling Proximity Identifier Key
+
+The Rolling Proximity Identifier Key (RPIK) is derived from the Temporary Exposure Key and is
+used in order to create the Rolling Proximity Identifiers.
+
+The key is generated in
+`com.google.samples.exposurenotification.crypto.KeyDerivation` via
+`com.google.samples.exposurenotification.data.generator.RollingProximityIdGeneratorBase#generateRpiKey`.
+
+```java
+static byte[] generateRpiKey(byte[] temporaryExposureKey) {
+    // RPIK = HKDF(tek, NULL, UTF8("EN-RPIK"), 16).
+    return KeyDerivation.hkdfSha256(
+            temporaryExposureKey,
+            /* inputSalt =*/ null,
+            "EN-RPIK".getBytes(UTF_8),
+            16);
+}
+```
+
+```java
+public static byte[] hkdfSha256(
+        Mac mac, byte[] inputKeyingMaterial,
+        @Nullable byte[] inputSalt,
+        byte[] info,
+        int length) throws CryptoException {
+
+    byte[] salt = (inputSalt == null || inputSalt.length == 0) ? new byte[HASH_LENGTH] : inputSalt;
+    try {
+        mac = Mac.getInstance("HmacSHA256");
+        return hkdfSha256ExpandLength16(mac, hkdfSha256Extract(mac, inputKeyingMaterial, salt), info);
+    } catch (InvalidKeyException e) {
+        throw new CryptoException(e);
+    }
+}
+```
+
+The method `hkdfSha256ExpandLength16` is implemented in the same class:
+
+```java
+private static byte[] hkdfSha256ExpandLength16(Mac mac, byte[] pseudoRandomKey, byte[] info)
+        throws InvalidKeyException {
+    // For length being 16 cases, counter always equals to 0x01.
+    byte[] counter = {0x01};
+    mac.init(new SecretKeySpec(pseudoRandomKey, ALGORITHM_NAME));
+    mac.update(info);
+
+    return Arrays.copyOf(mac.doFinal(counter), HKDF_OUTPUT_LENGTH);
+}
+```
+
+## Rolling Proximity Identifier
+
+Rolling Proximity Identifiers (RPIs) are short-lived identifiers (on average 15 minutes) that are broadcast in
+Bluetooth payloads. The RPIs are generated every 10 minutes, and, because of how Android
+BLE advertising works, each time it's started the system will also generate a new random BLE MAC address.
+
+The code to generate an RPI begins in 
+`com.google.samples.exposurenotification.data.generator.RollingProximityIdGeneratorBase` in the
+`generateId` method:
+
+```java
+public RollingProximityId generateId(int intervalNumber) throws CryptoException {
+    return new RollingProximityId(
+            encryptor.encrypt(generatePaddedData(intervalNumber, "EN-RPI")));
+}
+```
+
+`generatePaddedData` creates a `ByteBuffer` and calls through to the method `addPaddedData`:
+
+```java
+private static ByteBuffer addPaddedData(int intervalNumber, byte[] aesPadding) {
+    // AES_BLOCK_SIZE = 16 (bytes)
+    ByteBuffer buffer = ByteBuffer.allocate(AES_BLOCK_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+    buffer
+            .put(aesPadding)
+            .position(buffer.position() + AES_BLOCK_SIZE - aesPadding.length - Integer.BYTES);
+    buffer.putInt(intervalNumber);
+    return buffer;
+}
+```
+
+The encryption to generate a new RPI from a TEK seed is handled by the class
+`com.google.samples.exposurenotification.crypto.AesEcbEncryptor` in the `encrypt` method:
+
+```java
+public byte[] encrypt(byte[] data) throws CryptoException {
+    try {
+        Cipher cipher = new AesEcbEncryptor(Cipher.getInstance("AES/ECB/NoPadding"));
+        return cipher.doFinal(data);
+    } catch (BadPaddingException | IllegalBlockSizeException e) {
+        throw new CryptoException(e);
+    }
+}
+```
+
+## Associated Encrypted Metadata Key
+
+The Associated Encrypted Metadata Keys are derived from the Temporary Exposure Keys in order to
+encrypt additional metadata.
+
+The key is generated by the 
+`com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataHelper` class:
+
+```java
+public static byte[] generateAemKey(byte[] temporaryExposureKey) {
+    // AEMK = HKDF(tek, NULL, UTF8("EN-AEMK"), 16).
+    return KeyDerivation.hkdfSha256(
+            temporaryExposureKey,
+            /* inputSalt =*/ null,
+            "EN-AEMK".getBytes(UTF_8),
+            16);
+}
+```
+
+The `KeyDerivation.hkdfSha256` method is the same function used earlier to generate the [Rolling
+Proximity Identifier Key](#Rolling-Proximity-Identifier-Key).
+
+## Associated Encrypted Metadata
+
+The Associated Encrypted Metadata is data encrypted along with the Rolling Proximity Identifier,
+and can only be decrypted later if the user broadcasting it tested positive and reveals their
+Temporary Exposure Key.
+
+The details of the metadata itself are described in the Advertising Payload section of the
+[Bluetooth Specification](https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf).
+
+The encryption is performed by the 
+`com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataHelper` class:
+
+```java
+public static byte[] encryptOrDecrypt(
+        byte[] aemKey, byte[] rollingProximityId, byte[] bluetoothMetadataAsBytes)
+        throws CryptoException {
+    return AesCtrEncryptor.aesCtr(aemKey, rollingProximityId, bluetoothMetadataAsBytes);
+}
+```
+
+The encryption of the metadata itself is performed with an AES-128 block cipher in Counter Mode with the rolling
+proximity ID as the IV. 
+
+```java
+public static byte[] aesCtr(byte[] key, byte[] iv, byte[] data) throws CryptoException {
+    try {
+        Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
+        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
+
+        return cipher.doFinal(data);
+    } catch (NoSuchPaddingException
+            | NoSuchAlgorithmException
+            | InvalidAlgorithmParameterException
+            | InvalidKeyException
+            | BadPaddingException
+            | IllegalBlockSizeException e) {
+        throw new CryptoException(e);
+    }
+}
+```
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1af981f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2014 The Android Open Source Project
+
+   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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5a9d716
--- /dev/null
+++ b/README.md
@@ -0,0 +1,76 @@
+# Exposure Notifications API
+
+This repository contains a snapshot of code from Google Play Services' [Exposure Notifications
+ module][1].
+
+## Key Features
+
+There are a number of features in this source set, including abstrations and JNI. The following sections provide key features with pointers to the source code.
+
+### BLE MAC and RPI Rotation
+
+Code: [`com.google.samples.exposurenotification.ble.advertising.BleAdvertiser#startAdvertising`][2]
+
+Bluetooth Low Energy (BLE) MAC addresses rotate on average every 15 minutes to prevent remote location tracking that could be accomplished by tying together observations of a fixed MAC address.
+
+In order to best protect user privacy, the Exposure Notifications framework ensures that Rolling
+Proximity Identifiers (RPIs) are never rotated without also having a corresponding change of the
+Bluetooth MAC address. For more details, see the full [Bluetooth spec][3].
+
+Because Android doesn't have a callback to notify an application that the Bluetooth MAC address
+is changing (or has changed), this is handled by explicitly stopping and restarting advertising
+whenever a new RPI is generated.
+
+Since there isn't any callback, it is possible for the Bluetooth MAC address to rotate
+before a new RPI is generated. In this case the following would happen:
+
+|Time | Bluetooth MAC | RPI |
+|-----|---------------|-----|
+| :00 | 00:00:00:01   | AAA |
+| :09 | 00:00:00:02   | AAA |
+| :10 | 00:00:00:03   | BBB |
+
+The risk posed by this is minimal, since even though an observer may be able to tie the MAC
+addresses `00:00:00:01` and `00:00:00:02` together using the common RPI, the duration of both
+MAC addresses is no longer than a single RPI period (~10 minutes). When the RPI rotates, the
+MAC address rotates again, making it difficult to track the association of either the MAC
+address or RPI to a common device.
+
+### Rolling Proximity Identifier Generation
+
+Code: [`com.google.samples.exposurenotification.ble.advertising.RollingProximityIdManager#getCurrentRollingProximityId`](exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/RollingProximityIdManager.java)
+Code: [`com.google.samples.exposurenotification.data.generator.TemporaryExposureKeyGenerator`](exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/TemporaryExposureKeyGenerator.java)
+
+Rolling Proximity Identifiers (RPIs) are generated from a Temporary Exposure Key (TEK) based on the
+[Exposure Notification Cryptography Specification][4].
+
+For more information about TEKs and RPIs, see [Exposure Notifications Cryptography](CRYPTO.md#Temporary-Exposure-Key)
+
+### Associated Encrypted Metadata
+
+Code: [`com.google.samples.exposurenotification.ble.advertising.BleAdvertisementGenerator`](exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertisementGenerator.java)
+Code: [`com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataHelper`](exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataHelper.java)
+
+Associated Encrypted Metadata is generated by `BleAdvertisementGenerator#generatePacket` based on the
+[Exposure Notification Cryptography Specification][4]. This 
+uses `BluetoothMetadata#create` to create the metadata itself.
+
+The encryption, decryption, and generation of keys is handled by the
+class `AssociatedEncryptedMetadataHelper`.
+
+For more information about Associated Encrypted Metadata, see [Exposure Notifications Cryptography](CRYPTO.md#Associated-Encrypted-Metadata)
+
+### Key Matching
+
+Code: [`com.google.samples.exposurenotification.matching.MatchingJni`](exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingJni.java)
+
+The core process of Key Matching is started via `MatchingJni`. The class then calls into native C++ code to improve performance by avoiding Java binder calls for the many crypto operations needed.
+
+A `MatchingJni` object is initialized with a list of scanned RPIs from the past 14 days. The caller can pass a list of Diagnosis Key files, provided by the Healthcare Authority app, to the method `MatchingJni#matching`. This method produces a list of `TemporaryExposureKey`s corresponding to the Diagnosis Keys that the device was exposed to.
+
+[1]: https://developers.google.com/android/exposure-notifications/exposure-notifications-api
+[2]: exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertiser.java
+[3]: https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf
+[4]: https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf
+
+
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..64d49a8
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "30.0.1"
+    ndkVersion "21.3.6528147"
+
+    defaultConfig {
+        applicationId "com.google.samples.ensnippets"
+        minSdkVersion 21
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        externalNativeBuild {
+            cmake {
+                cppFlags "-std=c++14"
+            }
+        }
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    externalNativeBuild {
+        cmake {
+            path "src/main/cpp/CMakeLists.txt"
+            version "3.10.2"
+        }
+    }
+}
+
+dependencies {
+    implementation project(path: ':exposurenotification')
+
+    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+    implementation 'androidx.core:core-ktx:1.3.0'
+    implementation 'androidx.appcompat:appcompat:1.1.0'
+    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+
+    testImplementation 'junit:junit:4.13'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/google/samples/ensnippets/RoundTripTests.kt b/app/src/androidTest/java/com/google/samples/ensnippets/RoundTripTests.kt
new file mode 100644
index 0000000..ba17988
--- /dev/null
+++ b/app/src/androidTest/java/com/google/samples/ensnippets/RoundTripTests.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.ensnippets
+
+import androidx.core.util.Supplier
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey
+import com.google.samples.exposurenotification.ble.advertising.RollingProximityIdManager
+import com.google.samples.exposurenotification.ble.interfaces.BleDatabaseWriter
+import com.google.samples.exposurenotification.features.ContactTracingFeature
+import com.google.samples.exposurenotification.matching.MatchingJni
+import com.google.samples.exposurenotification.storage.SelfTemporaryExposureKeyDataStore
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.FileOutputStream
+import java.math.BigInteger
+import java.util.concurrent.TimeUnit
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class RoundTripTests {
+    private lateinit var exportKeyFile: File
+
+    @Before
+    fun setup() {
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        appContext.resources.openRawResource(R.raw.export).use { inputStream ->
+            exportKeyFile = File(appContext.filesDir, "export.bin")
+            FileOutputStream(exportKeyFile).use { fileOutputStream ->
+                inputStream.copyTo(fileOutputStream)
+            }
+        }
+    }
+
+    @After
+    fun teardown() {
+        if (exportKeyFile.exists()) exportKeyFile.delete()
+    }
+
+    /**
+     * This is a test that shows how to call the native matching code. This test assumes that the
+     * ZIP file of diagnosis keys has been verified and the diagnosis keys have been extracted
+     * to the file referenced by [exportKeyFile].
+     */
+    @Test
+    fun testGenerateAndMatchRPI() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+
+        // Required parameter, but is unused in this test.
+        val testBleDatabaseWriter = TestBleDatabaseWriter()
+
+        // In order to match a key provided in the file, we've hardcoded one of the provided
+        // diagnosis keys here (0x2b714d3ec93c14e5d345003e59027aea).
+        //
+        // We inject this key into a SelfTemporaryExposureKeyDataStore and then use the code
+        // that generates Rolling Proximity Ids (RPIs) to create an RPI from the diagnosis key
+        // above.
+        val testDataStore =
+            TestSelfTemporaryExposureKeyDataStore("2b714d3ec93c14e5d345003e59027aea", 2646801)
+        val rpiManager = RollingProximityIdManager(
+            testDataStore,
+            testBleDatabaseWriter,
+            Supplier { rollingPeriodStartToMillis(2646801) }
+        )
+
+        // Here we use the RPI manager created above to get an RPI
+        val seenRpi = rpiManager.currentRollingProximityId.get()
+
+        // We take this key and say it's a key that we saw during scanning and provide that
+        // to the matching code.
+        val bleScanResults = arrayOf(seenRpi)
+        val matchingJni = MatchingJni(appContext, bleScanResults)
+
+        // We then call through and provide the diagnosis keys file we extracted
+        val results = matchingJni.matching(listOf(exportKeyFile.toString()))
+
+        // Because we generated an RPI from one of the diagnosis keys in the test data, we
+        // expect that we have seen exactly 1 result (since we generated one RPI)
+        Assert.assertEquals(results.size, 1)
+
+        // We check the actual results and make sure that the key that's matched is the
+        // one that was generated above.
+        // (This also ensures that the native code which generates RPIs from a diagnosis key
+        // creates the same keys as the Java code which generates them from a temporary
+        // exposure key.)
+        results.forEach { matchedKey ->
+            Assert.assertArrayEquals(
+                matchedKey.keyData,
+                keyStringToByteArray("2b714d3ec93c14e5d345003e59027aea")
+            )
+        }
+    }
+
+    private fun rollingPeriodStartToMillis(rollingPeriod: Int) =
+        rollingPeriod *
+                TimeUnit.MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes().toLong())
+}
+
+// The rolling period _could_ be shorter if we wanted to expire it early (so that we could, for
+// example, share a positive diagnosis ASAP. For testing we just set it to 144 (24 hours).
+private const val TEST_ROLLING_PERIOD = 144
+
+class TestSelfTemporaryExposureKeyDataStore(
+    hexKey: String,
+    rollingPeriodStart: Int
+) : SelfTemporaryExposureKeyDataStore() {
+
+    init {
+        val testKey = TemporaryExposureKey.TemporaryExposureKeyBuilder()
+            .setKeyData(keyStringToByteArray(hexKey))
+            .setRollingStartIntervalNumber(rollingPeriodStart)
+            .setRollingPeriod(TEST_ROLLING_PERIOD)
+            .build()
+        putKey(testKey)
+    }
+}
+
+class TestBleDatabaseWriter : BleDatabaseWriter {
+    override fun writeNewTemporaryExposureKey(temporaryExposureKey: TemporaryExposureKey?) {
+        // Nothing to store for the test.
+    }
+
+    override fun writeBleSighting(
+        rollingProximityId: ByteArray?,
+        rssi: Int,
+        associatedEncryptedMetadata: ByteArray?,
+        previousScanEpochSeconds: Int
+    ) {
+        // Nothing needed for testing.
+    }
+}
+
+/**
+ * Helper method to take a hex string (key) and convert it to a byte array.
+ */
+private fun keyStringToByteArray(key: String): ByteArray = BigInteger(key, 16).toByteArray()
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/google/samples/ensnippets/SignatureVerificationTests.kt b/app/src/androidTest/java/com/google/samples/ensnippets/SignatureVerificationTests.kt
new file mode 100644
index 0000000..b679c4a
--- /dev/null
+++ b/app/src/androidTest/java/com/google/samples/ensnippets/SignatureVerificationTests.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.ensnippets
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.samples.exposurenotification.matching.ProvideDiagnosisKeys
+import org.junit.After
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.FileOutputStream
+
+/** Sample app package name */
+private const val SAMPLE_PACKAGE = "com.google.android.apps.exposurenotification"
+
+@RunWith(AndroidJUnit4::class)
+class SignatureVerificationTests {
+    private lateinit var diagnosisKeyFile: File
+
+    @Before
+    fun setup() {
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        appContext.resources.openRawResource(R.raw.sample_diagnosis_key_file).use { inputStream ->
+            diagnosisKeyFile = File(appContext.filesDir, "export.zip")
+            FileOutputStream(diagnosisKeyFile).use { fileOutputStream ->
+                inputStream.copyTo(fileOutputStream)
+            }
+        }
+
+        val targetFolder = File(appContext.filesDir, "en_diagnosis_keys")
+        targetFolder.deleteRecursively()
+    }
+
+    @After
+    fun teardown() {
+        if (diagnosisKeyFile.exists()) diagnosisKeyFile.delete()
+    }
+
+    @Test
+    fun testVerifyDiagnosisKeysZipFile() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        val provideDiagnosisKeys = ProvideDiagnosisKeys(appContext)
+
+        assertTrue(provideDiagnosisKeys.verify(SAMPLE_PACKAGE, diagnosisKeyFile))
+    }
+}
diff --git a/app/src/androidTest/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGeneratorTest.java b/app/src/androidTest/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGeneratorTest.java
new file mode 100644
index 0000000..176e8a5
--- /dev/null
+++ b/app/src/androidTest/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGeneratorTest.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data.generator;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.crypto.AesEcbEncryptor;
+import com.google.samples.exposurenotification.data.DayNumber;
+import com.google.samples.exposurenotification.data.GeneratedRollingProximityId;
+import com.google.samples.exposurenotification.data.RollingProximityId;
+import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
+import com.google.samples.exposurenotification.data.generator.RollingProximityIdGeneratorBase.PaddedDataCache;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+import com.google.samples.exposurenotification.testing.TestVectors;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4.class)
+public class RollingProximityIdGeneratorTest {
+
+    @Test
+    public void generateId_usingTestVector() throws Exception {
+        byte[] temporaryTracingKey = TestVectors.get_TEMPORARY_TRACING_KEY();
+        TemporaryExposureKey temporaryExposureKey =
+                new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                        .setRollingStartIntervalNumber(0)
+                        .setKeyData(temporaryTracingKey)
+                        .build();
+        int ctIntervalNumber = TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY;
+        RollingProximityIdGenerator generator =
+                new RollingProximityIdGenerator(
+                        temporaryExposureKey.getKeyData(),
+                        temporaryExposureKey.getRollingStartIntervalNumber(),
+                        TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey),
+                        ContactTracingFeature.rollingProximityIdKeySizeBytes(),
+                        ContactTracingFeature.rpikHkdfInfoString(),
+                        ContactTracingFeature.rpidAesPaddedString());
+        for (TestVectors.AdvertisedData element : TestVectors.ADVERTISED_DATA) {
+            RollingProximityId generatedId = generator.generateId(ctIntervalNumber);
+            assertArrayEquals(generatedId.get(), element.get_RPI());
+            ctIntervalNumber++;
+        }
+    }
+
+    @Test
+    public void testGenerateIds_generatesCorrectIds() throws Exception {
+        TemporaryExposureKey temporaryExposureKey =
+                new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                        .setRollingStartIntervalNumber(TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY)
+                        .setRollingPeriod(TestVectors.ADVERTISED_DATA.size())
+                        .setKeyData(TestVectors.get_TEMPORARY_TRACING_KEY())
+                        .build();
+
+        List<GeneratedRollingProximityId> generatedIds =
+                new RollingProximityIdGenerator(
+                        temporaryExposureKey.getKeyData(),
+                        temporaryExposureKey.getRollingStartIntervalNumber(),
+                        TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey),
+                        ContactTracingFeature.rollingProximityIdKeySizeBytes(),
+                        ContactTracingFeature.rpikHkdfInfoString(),
+                        ContactTracingFeature.rpidAesPaddedString())
+                        .generateIds(
+                                new byte
+                                        [ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod()
+                                        * ContactTracingFeature.contactIdLength()]);
+
+        assertEquals(generatedIds.size(), TestVectors.ADVERTISED_DATA.size());
+        for (int i = 0; i < TestVectors.ADVERTISED_DATA.size(); i++) {
+            assertArrayEquals(generatedIds.get(i).rollingProximityId().get(),
+                    TestVectors.ADVERTISED_DATA.get(i).get_RPI());
+        }
+    }
+
+    // Runs the RPIK test vector on the spec:
+    @Test
+    public void generateRpiKey_usingTestVector() throws Exception {
+        byte[] rpiKey =
+                RollingProximityIdGenerator.generateRpiKey(
+                        TestVectors.get_TEMPORARY_TRACING_KEY(),
+                        TestVectors.get_RPIK_HKDF_INFO_STRING(),
+                        ContactTracingFeature.rollingProximityIdKeySizeBytes());
+        assertArrayEquals(rpiKey, TestVectors.get_RPIK());
+    }
+
+    @Test
+    public void getCachedData_sameEnIntervalNumber_sameAsDirectlyGenerated() {
+        int idsPerKey =
+                ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
+        int baseEnIntervalNumber =
+                TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY
+                        - (ContactTracingFeature.contactRecordDaysToKeep() * idsPerKey);
+        int cacheSize = ContactTracingFeature.contactRecordDaysToKeep() + 1;
+        byte[] aesPadding = ContactTracingFeature.rpidAesPaddedString().getBytes(UTF_8);
+
+        PaddedDataCache paddedDataCache =
+                PaddedDataCache.createInstance(baseEnIntervalNumber, cacheSize, aesPadding, idsPerKey);
+
+        for (int i = 0, testEnIntervalNumber = baseEnIntervalNumber;
+             i < cacheSize;
+             i++, testEnIntervalNumber += idsPerKey) {
+            assertArrayEquals(paddedDataCache.getCachedData(testEnIntervalNumber),
+                    RollingProximityIdGeneratorBase.generateAggregatePaddedData(
+                            testEnIntervalNumber, idsPerKey, aesPadding));
+        }
+    }
+
+    @Test
+    public void getCachedData_notAlignedEnIntervalNumber_mustReturnNull() {
+        int idsPerKey =
+                ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
+        int baseEnIntervalNumber =
+                TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY
+                        - (ContactTracingFeature.contactRecordDaysToKeep() * idsPerKey);
+        int cacheSize = ContactTracingFeature.contactRecordDaysToKeep() + 1;
+        byte[] aesPadding = ContactTracingFeature.rpidAesPaddedString().getBytes(UTF_8);
+
+        PaddedDataCache paddedDataCache =
+                PaddedDataCache.createInstance(baseEnIntervalNumber, cacheSize, aesPadding, idsPerKey);
+
+        assertEquals(paddedDataCache.getCachedData(baseEnIntervalNumber + 1), null);
+        assertEquals(paddedDataCache.getCachedData(
+                TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY - 1), null);
+    }
+
+    @Test
+    public void getCachedData_outOfRangeEnIntervalNumber_mustReturnNull() {
+        int idsPerKey =
+                ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
+        int baseEnIntervalNumber =
+                TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY
+                        - (ContactTracingFeature.contactRecordDaysToKeep() * idsPerKey);
+        int cacheSize = ContactTracingFeature.contactRecordDaysToKeep() + 1;
+        byte[] aesPadding = ContactTracingFeature.rpidAesPaddedString().getBytes(UTF_8);
+
+        PaddedDataCache paddedDataCache =
+                PaddedDataCache.createInstance(baseEnIntervalNumber, cacheSize, aesPadding, idsPerKey);
+
+        assertEquals(paddedDataCache.getCachedData(baseEnIntervalNumber - idsPerKey), null);
+        assertEquals(
+                paddedDataCache.getCachedData(
+                        TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY + idsPerKey), null);
+    }
+
+    @Test
+    public void generateIds_numIdsTooLarge_shouldNotCrash() throws Exception {
+        TemporaryExposureKey temporaryExposureKey =
+                new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                        .setRollingStartIntervalNumber(0)
+                        .setRollingPeriod(145)
+                        .setKeyData(
+                                TemporaryExposureKeySupport.getMinKey(DayNumber.MIN_DAY_NUMBER).getKeyData())
+                        .setTransmissionRiskLevel(1)
+                        .build();
+
+        List<GeneratedRollingProximityId> generatedIds =
+                new RollingProximityIdGenerator.Factory()
+                        .getInstance(
+                                AesEcbEncryptor.create(),
+                                temporaryExposureKey.getKeyData(),
+                                temporaryExposureKey.getRollingStartIntervalNumber(),
+                                TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey),
+                                ContactTracingFeature.rollingProximityIdKeySizeBytes(),
+                                ContactTracingFeature.rpikHkdfInfoString(),
+                                ContactTracingFeature.rpidAesPaddedString())
+                        .generateIds(
+                                new byte[145 * ContactTracingFeature.contactIdLength()]);
+        assertEquals(generatedIds.size(), 145);
+    }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/google/samples/exposurenotification/testing/AssociatedEncryptedMetadataGeneratorTest.java b/app/src/androidTest/java/com/google/samples/exposurenotification/testing/AssociatedEncryptedMetadataGeneratorTest.java
new file mode 100644
index 0000000..ceefba2
--- /dev/null
+++ b/app/src/androidTest/java/com/google/samples/exposurenotification/testing/AssociatedEncryptedMetadataGeneratorTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.testing;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
+import com.google.samples.exposurenotification.data.BluetoothMetadata;
+import com.google.samples.exposurenotification.data.RollingProximityId;
+import com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataGenerator;
+import com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataHelper;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(AndroidJUnit4.class)
+public class AssociatedEncryptedMetadataGeneratorTest {
+
+    @Test
+    public void decryptEncryptedBleMetadata_mustGetOriginalValue() throws Exception {
+        RollingProximityId rollingProximityId =
+                new RollingProximityId(base16().decode("AABBCCDDEEFF00112233445566778899"));
+        TemporaryExposureKey temporaryExposureKey =
+                new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                        .setKeyData(base16().decode("FEDCBA9876543210FEDCBA9876543210"))
+                        .setRollingStartIntervalNumber(0)
+                        .build();
+        BluetoothMetadata metadata = BluetoothMetadata.fromBytes(new byte[]{0x01, 0x02, 0x03, 0x04});
+
+        AssociatedEncryptedMetadataGenerator generator =
+                new AssociatedEncryptedMetadataGenerator(temporaryExposureKey);
+        AssociatedEncryptedMetadata encryptedMetadata =
+                generator.encrypt(rollingProximityId, metadata);
+
+        assertEquals(generator.decrypt(rollingProximityId, encryptedMetadata), metadata);
+    }
+
+    @Test
+    public void encrypt_usingTestVector() throws Exception {
+        byte[] aemKey = TestVectors.get_AEMK();
+        byte[] bleMetadata = TestVectors.get_BLE_METADATA();
+
+        for (TestVectors.AdvertisedData element : TestVectors.ADVERTISED_DATA) {
+            byte[] encryptedMetadata =
+                    AssociatedEncryptedMetadataHelper.encryptOrDecrypt(
+                            aemKey, element.get_RPI(), bleMetadata);
+            assertArrayEquals(encryptedMetadata, element.get_AEM());
+        }
+    }
+
+    @Test
+    public void decrypt_usingTestVector() throws Exception {
+        byte[] aemKey = TestVectors.get_AEMK();
+        byte[] bleMetadata = TestVectors.get_BLE_METADATA();
+
+        for (TestVectors.AdvertisedData element : TestVectors.ADVERTISED_DATA) {
+            byte[] decryptedMetadata =
+                    AssociatedEncryptedMetadataHelper.encryptOrDecrypt(
+                            aemKey, element.get_RPI(), element.get_AEM());
+            assertArrayEquals(decryptedMetadata, bleMetadata);
+        }
+    }
+
+    // Runs the AEMK test vector on the spec:
+    @Test
+    public void generateAemKey_usingTestVector() throws Exception {
+        byte[] aemKey =
+                AssociatedEncryptedMetadataHelper.generateAemKey(
+                        TestVectors.get_TEMPORARY_TRACING_KEY(),
+                        ContactTracingFeature.aemkHkdfInfoString(),
+                        ContactTracingFeature.associatedMetadataEncryptionKeySizeBytes());
+        assertArrayEquals(aemKey, TestVectors.get_AEMK());
+    }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/google/samples/exposurenotification/testing/TestVectors.java b/app/src/androidTest/java/com/google/samples/exposurenotification/testing/TestVectors.java
new file mode 100644
index 0000000..dd03b13
--- /dev/null
+++ b/app/src/androidTest/java/com/google/samples/exposurenotification/testing/TestVectors.java
@@ -0,0 +1,1008 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.testing;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Range;
+
+import org.junit.Assert;
+
+/**
+ * Java version of Exposure Notification test vectors (v1.2).
+ *
+ * <p>{@link #get_TEMPORARY_TRACING_KEY} is a random key generated at {@link
+ * #KEY_GENERATION_NSECONDS}.
+ *
+ * <p>Derived from those values:
+ *
+ * <ul>
+ *   <li>{@link #CTINTERVAL_NUMBER_OF_GENERATED_KEY} is derived from the generation time.
+ *   <li>{@link #get_RPIK} returns the expected Rolling Proximity Indicator Key
+ *   <li>{@link #get_AEMK} returns the expected Associated Metadata Encryption Key
+ * </ul>
+ *
+ * <p>Given the associated metadata in {@link #get_BLE_METADATA}, the expected values at each 10
+ * minute interval are provided in {@link #ADVERTISED_DATA}, e.g.
+ *
+ * <pre>{@code
+ * for (int i = 0; i < 144; i++) {
+ *   byte[] rpi = generateRpi(i, TestVectors.get_RPIK());
+ *   byte[] aem = generateAem(i, TestVectors.get_AEMK(), testVectors,get_BLE_METADATA());
+ *   assertThat(rpi).isEqualTo(ADVERTISED_DATA[i].get_RPI());
+ *   assertThat(aem).isEqualTo(ADVERTISED_DATA[i].get_AEM());
+ * }
+ * }</pre>
+ *
+ * <p>All of the byte array "constants" are accessed through static functions which return a fresh
+ * copy of the master array, to avoid any possibility of corruption by the tests.
+ *
+ * <p>The original C++ header is in TestVectors.h.txt.
+ */
+public class TestVectors {
+
+    public static byte[] asBytes(int... ints) {
+        byte[] bytes = new byte[ints.length];
+        for (int i = 0; i < ints.length; i++) {
+            int value = ints[i];
+            Assert.assertTrue(Range.closed(0, 255).contains(value));
+            bytes[i] = (byte) value;
+        }
+        return bytes;
+    }
+
+    /**
+     * Class holding a matched pair of RPI and AEM values.
+     */
+    public static class AdvertisedData {
+        private final byte[] rollingProximityIndicator;
+        private final byte[] associatedEncryptedMetadata;
+
+        AdvertisedData(byte[] rollingProximityIndicator, byte[] associatedEncryptedMetadata) {
+            this.rollingProximityIndicator = rollingProximityIndicator;
+            this.associatedEncryptedMetadata = associatedEncryptedMetadata;
+        }
+
+        public byte[] get_RPI() {
+            return rollingProximityIndicator.clone();
+        }
+
+        public byte[] get_AEM() {
+            return associatedEncryptedMetadata.clone();
+        }
+    }
+
+    public static byte[] get_TEMPORARY_TRACING_KEY() {
+        return TEMPORARY_TRACING_KEY.clone();
+    }
+
+    public static byte[] get_RPIK() {
+        return RPIK.clone();
+    }
+
+    public static byte[] get_AEMK() {
+        return AEMK.clone();
+    }
+
+    public static byte[] get_BLE_METADATA() {
+        return BLE_METADATA.clone();
+    }
+
+    public static String get_RPIK_HKDF_INFO_STRING() {
+        return "EN-RPIK";
+    }
+
+    public static String get_RPI_AES_PADDED_STRING() {
+        return "EN-RPI";
+    }
+
+    public static String get_AEMK_HKDF_INFO_STRING() {
+        return "EN-AEMK";
+    }
+
+    // From TestVectors.h.txt
+    // ------------------------------------------------------------------------------
+    public static final int KEY_GENERATION_NSECONDS = 1585785600;
+    public static final int CTINTERVAL_NUMBER_OF_GENERATED_KEY = 2642976;
+    public static final int ID_ROLLING_PERIOD_MINUTES = 10;
+    public static final int KEY_ROLLING_PERIOD_MULTIPLE_OF_ID_PERIOD = 144;
+
+    private static final byte[] TEMPORARY_TRACING_KEY =
+            asBytes(
+                    0x75, 0xc7, 0x34, 0xc6, 0xdd, 0x1a, 0x78, 0x2d, 0xe7, 0xa9, 0x65, 0xda, 0x5e, 0xb9, 0x31,
+                    0x25);
+
+    private static final byte[] RPIK =
+            asBytes(
+                    0x18, 0x5a, 0xd9, 0x1d, 0xb6, 0x9e, 0xc7, 0xdd, 0x04, 0x89, 0x60, 0xf1, 0xf3, 0xba, 0x61,
+                    0x75);
+    private static final byte[] AEMK =
+            asBytes(
+                    0xd5, 0x7c, 0x46, 0xaf, 0x7a, 0x1d, 0x83, 0x96, 0x5b, 0x9b, 0xed, 0x8b, 0xd1, 0x52, 0x93,
+                    0x6a);
+    private static final byte[] BLE_METADATA = asBytes(0x40, 0x08, 0x00, 0x00);
+    private static final byte[] RPI0 =
+            asBytes(
+                    0x8b, 0xe6, 0xcd, 0x37, 0x1c, 0x5c, 0x89, 0x16, 0x04, 0xbf, 0xbe, 0x49, 0xdf, 0x84, 0x50,
+                    0x96);
+    private static final byte[] AEM0 = asBytes(0x72, 0x03, 0x38, 0x74);
+    private static final byte[] RPI1 =
+            asBytes(
+                    0x3c, 0x9a, 0x1d, 0xe5, 0xdd, 0x6b, 0x02, 0xaf, 0xa7, 0xfd, 0xed, 0x7b, 0x57, 0x0b, 0x3e,
+                    0x56);
+    private static final byte[] AEM1 = asBytes(0xc2, 0x92, 0x11, 0xb1);
+    private static final byte[] RPI2 =
+            asBytes(
+                    0x24, 0x3f, 0xfe, 0x9a, 0x3b, 0x08, 0xbd, 0xed, 0x30, 0x94, 0xba, 0xc8, 0x63, 0x0b, 0xb8,
+                    0xad);
+    private static final byte[] AEM2 = asBytes(0x6a, 0xdf, 0xad, 0x03);
+    private static final byte[] RPI3 =
+            asBytes(
+                    0xdf, 0xc3, 0xed, 0x26, 0x5e, 0x97, 0xd0, 0xea, 0xbb, 0x63, 0x0e, 0x16, 0x8b, 0x42, 0x14,
+                    0xed);
+    private static final byte[] AEM3 = asBytes(0xf1, 0xe2, 0xf8, 0x0b);
+    private static final byte[] RPI4 =
+            asBytes(
+                    0xb3, 0xb8, 0x5a, 0x69, 0xeb, 0xae, 0xc7, 0x8d, 0xb7, 0x39, 0x85, 0x2d, 0x1f, 0x34, 0xe0,
+                    0xfa);
+    private static final byte[] AEM4 = asBytes(0x56, 0x68, 0xc4, 0x74);
+    private static final byte[] RPI5 =
+            asBytes(
+                    0x29, 0x8a, 0xbd, 0x6f, 0xda, 0xd2, 0x9e, 0xfb, 0xf0, 0xf8, 0x5a, 0x63, 0x95, 0x6c, 0xf1,
+                    0x88);
+    private static final byte[] AEM5 = asBytes(0xf7, 0xb9, 0x7e, 0x84);
+    private static final byte[] RPI6 =
+            asBytes(
+                    0x10, 0x5e, 0x82, 0x21, 0xdd, 0x60, 0x3d, 0x25, 0xb9, 0x4a, 0xba, 0x0c, 0x3c, 0xc8, 0xde,
+                    0xe1);
+    private static final byte[] AEM6 = asBytes(0xee, 0xab, 0xfd, 0xc7);
+    private static final byte[] RPI7 =
+            asBytes(
+                    0x98, 0x88, 0xca, 0x7e, 0x67, 0x38, 0xec, 0x4b, 0xc6, 0xe4, 0x20, 0xb2, 0x0f, 0x87, 0x8b,
+                    0x3a);
+    private static final byte[] AEM7 = asBytes(0x9f, 0x84, 0xa9, 0xa6);
+    private static final byte[] RPI8 =
+            asBytes(
+                    0x86, 0x94, 0xa7, 0x9a, 0xe9, 0x96, 0x71, 0xbe, 0x3f, 0x18, 0xaa, 0xf6, 0xb0, 0x90, 0x65,
+                    0x7a);
+    private static final byte[] AEM8 = asBytes(0xc1, 0x1a, 0xcb, 0x4d);
+    private static final byte[] RPI9 =
+            asBytes(
+                    0xb8, 0xcd, 0xe2, 0x8a, 0xd2, 0x0d, 0x1c, 0xd2, 0xfd, 0xd7, 0x36, 0x9d, 0xc0, 0xc6, 0xf7,
+                    0xc8);
+    private static final byte[] AEM9 = asBytes(0x1f, 0x8d, 0x53, 0xde);
+    private static final byte[] RPI10 =
+            asBytes(
+                    0xf6, 0x01, 0x23, 0xc6, 0x7b, 0xd2, 0xfc, 0x4b, 0x62, 0x00, 0x2b, 0x4b, 0x7d, 0x59, 0x5b,
+                    0xa6);
+    private static final byte[] AEM10 = asBytes(0xe0, 0xa7, 0xf9, 0xf2);
+    private static final byte[] RPI11 =
+            asBytes(
+                    0x8a, 0x84, 0xc8, 0x6e, 0x05, 0xf7, 0xa8, 0x77, 0x2a, 0xae, 0x7a, 0x80, 0x68, 0x6e, 0x1a,
+                    0x1c);
+    private static final byte[] AEM11 = asBytes(0xf4, 0xb7, 0xb0, 0x9d);
+    private static final byte[] RPI12 =
+            asBytes(
+                    0xac, 0x46, 0x1b, 0xf2, 0xb9, 0x3b, 0x0d, 0x90, 0x84, 0x17, 0x46, 0xf5, 0x1a, 0xde, 0xd6,
+                    0xc0);
+    private static final byte[] AEM12 = asBytes(0x73, 0x91, 0xdd, 0x64);
+    private static final byte[] RPI13 =
+            asBytes(
+                    0xf2, 0x56, 0x7e, 0xed, 0x0a, 0xa1, 0xcd, 0xf2, 0xcd, 0x3d, 0x0b, 0xd2, 0x82, 0x52, 0xf1,
+                    0x96);
+    private static final byte[] AEM13 = asBytes(0x12, 0x7d, 0xb0, 0x9e);
+    private static final byte[] RPI14 =
+            asBytes(
+                    0xa2, 0x55, 0x22, 0x5b, 0xaa, 0x9e, 0x37, 0xb7, 0x30, 0xa9, 0x5f, 0x99, 0x7a, 0x69, 0x72,
+                    0xf5);
+    private static final byte[] AEM14 = asBytes(0x43, 0x5e, 0xa5, 0x56);
+    private static final byte[] RPI15 =
+            asBytes(
+                    0x04, 0xcf, 0xae, 0xe4, 0x10, 0x21, 0xcb, 0x7d, 0x4d, 0x02, 0x0b, 0x30, 0x6b, 0x24, 0xbe,
+                    0xa8);
+    private static final byte[] AEM15 = asBytes(0xdc, 0x1f, 0x19, 0x06);
+    private static final byte[] RPI16 =
+            asBytes(
+                    0xf5, 0x37, 0x2b, 0xbc, 0x92, 0xf7, 0x80, 0x59, 0x64, 0x70, 0x1e, 0x87, 0x9b, 0x48, 0xd4,
+                    0x31);
+    private static final byte[] AEM16 = asBytes(0xb3, 0xea, 0xaf, 0xdd);
+    private static final byte[] RPI17 =
+            asBytes(
+                    0xf2, 0xc8, 0x11, 0x54, 0x63, 0x82, 0xb1, 0x0d, 0xf1, 0xac, 0x06, 0xc3, 0x2c, 0x61, 0x7b,
+                    0xa7);
+    private static final byte[] AEM17 = asBytes(0xcd, 0x51, 0x86, 0xf9);
+    private static final byte[] RPI18 =
+            asBytes(
+                    0xf8, 0xf3, 0x44, 0x1f, 0x76, 0x22, 0x3d, 0xac, 0x15, 0xec, 0x6b, 0x35, 0xfd, 0xb2, 0x51,
+                    0x40);
+    private static final byte[] AEM18 = asBytes(0x7f, 0x38, 0x7e, 0x7f);
+    private static final byte[] RPI19 =
+            asBytes(
+                    0xd0, 0x71, 0x83, 0xdb, 0x3c, 0x80, 0x45, 0x08, 0x7d, 0x61, 0xee, 0x9e, 0x73, 0x0c, 0x93,
+                    0x06);
+    private static final byte[] AEM19 = asBytes(0x1a, 0x42, 0xa1, 0x5b);
+    private static final byte[] RPI20 =
+            asBytes(
+                    0xc7, 0x42, 0xdd, 0x9c, 0x96, 0xa3, 0xe6, 0xfa, 0x7c, 0x4f, 0x22, 0x62, 0x1d, 0xac, 0xc2,
+                    0x4d);
+    private static final byte[] AEM20 = asBytes(0x06, 0x39, 0x0d, 0xed);
+    private static final byte[] RPI21 =
+            asBytes(
+                    0x08, 0x23, 0x33, 0xfa, 0xd9, 0xfa, 0x29, 0x2a, 0xb8, 0x99, 0xd6, 0x00, 0x0c, 0x65, 0x97,
+                    0x97);
+    private static final byte[] AEM21 = asBytes(0x0f, 0x01, 0xbc, 0xd7);
+    private static final byte[] RPI22 =
+            asBytes(
+                    0xbe, 0x43, 0x00, 0xda, 0xfb, 0x8d, 0x07, 0xc8, 0x8c, 0xb2, 0xb5, 0x07, 0x7a, 0x06, 0x11,
+                    0x66);
+    private static final byte[] AEM22 = asBytes(0xc7, 0xbf, 0xbb, 0x92);
+    private static final byte[] RPI23 =
+            asBytes(
+                    0x5f, 0xed, 0x1b, 0x4d, 0x3b, 0x3a, 0x13, 0x33, 0x2f, 0x05, 0x44, 0x75, 0x60, 0x35, 0x26,
+                    0x32);
+    private static final byte[] AEM23 = asBytes(0x21, 0x7c, 0x8e, 0x4a);
+    private static final byte[] RPI24 =
+            asBytes(
+                    0xfd, 0x1e, 0xe2, 0xcc, 0x5c, 0x60, 0xe6, 0xee, 0xe6, 0x1f, 0x04, 0x91, 0x9f, 0x67, 0x59,
+                    0xa7);
+    private static final byte[] AEM24 = asBytes(0xee, 0x63, 0x9b, 0xd6);
+    private static final byte[] RPI25 =
+            asBytes(
+                    0x96, 0xad, 0xf5, 0xb8, 0xdc, 0x7e, 0xe7, 0x5d, 0xf4, 0x6f, 0xbd, 0x8a, 0x1f, 0xc4, 0xad,
+                    0x0d);
+    private static final byte[] AEM25 = asBytes(0x60, 0x8c, 0x13, 0x0f);
+    private static final byte[] RPI26 =
+            asBytes(
+                    0xfa, 0x4b, 0xa2, 0x20, 0x6d, 0x42, 0xa1, 0xc8, 0x0d, 0x52, 0x48, 0xae, 0x68, 0x83, 0x09,
+                    0xa4);
+    private static final byte[] AEM26 = asBytes(0x74, 0xb2, 0xc8, 0x73);
+    private static final byte[] RPI27 =
+            asBytes(
+                    0xa5, 0x90, 0xf9, 0x5d, 0xbf, 0x24, 0x02, 0x61, 0xda, 0x10, 0x1a, 0x7c, 0xdb, 0x24, 0xdb,
+                    0xba);
+    private static final byte[] AEM27 = asBytes(0x95, 0x6a, 0x95, 0xeb);
+    private static final byte[] RPI28 =
+            asBytes(
+                    0x67, 0xe8, 0x1b, 0x91, 0xd1, 0xcf, 0x9e, 0x09, 0x58, 0x13, 0x54, 0x29, 0xda, 0xd0, 0x1e,
+                    0x82);
+    private static final byte[] AEM28 = asBytes(0xc6, 0xfa, 0x3c, 0x7a);
+    private static final byte[] RPI29 =
+            asBytes(
+                    0x18, 0x3c, 0xac, 0x22, 0x36, 0xc3, 0xe0, 0x53, 0x3b, 0xe4, 0x70, 0x4d, 0x83, 0x6e, 0x47,
+                    0x55);
+    private static final byte[] AEM29 = asBytes(0xa2, 0xab, 0xc7, 0x03);
+    private static final byte[] RPI30 =
+            asBytes(
+                    0x34, 0x11, 0x62, 0x55, 0x1c, 0x29, 0x31, 0x9b, 0xc5, 0x35, 0x38, 0xed, 0xfc, 0xf2, 0x30,
+                    0x40);
+    private static final byte[] AEM30 = asBytes(0x80, 0x09, 0xb2, 0xaa);
+    private static final byte[] RPI31 =
+            asBytes(
+                    0x22, 0x51, 0x68, 0x70, 0x18, 0x21, 0x4b, 0x65, 0xdd, 0x8e, 0xe8, 0x3e, 0xae, 0xd3, 0x30,
+                    0xab);
+    private static final byte[] AEM31 = asBytes(0xcf, 0xe8, 0x04, 0xcb);
+    private static final byte[] RPI32 =
+            asBytes(
+                    0xa4, 0xcf, 0x6e, 0x50, 0x21, 0x5f, 0xe2, 0x78, 0xcc, 0x5c, 0xff, 0x1b, 0x05, 0x34, 0xa3,
+                    0xe0);
+    private static final byte[] AEM32 = asBytes(0xcb, 0x2a, 0x7e, 0x22);
+    private static final byte[] RPI33 =
+            asBytes(
+                    0xdf, 0x8f, 0x2a, 0xc3, 0x03, 0x23, 0x2b, 0x2e, 0x5b, 0x3e, 0xfd, 0x86, 0x81, 0xaa, 0xa8,
+                    0xdd);
+    private static final byte[] AEM33 = asBytes(0x65, 0x03, 0xa7, 0x27);
+    private static final byte[] RPI34 =
+            asBytes(
+                    0xba, 0x2e, 0x75, 0xd7, 0xf4, 0x8c, 0xf5, 0x5c, 0x0c, 0x86, 0x8f, 0xd4, 0x5c, 0xf1, 0x6b,
+                    0x5c);
+    private static final byte[] AEM34 = asBytes(0x8f, 0x29, 0x78, 0x7e);
+    private static final byte[] RPI35 =
+            asBytes(
+                    0xec, 0x6a, 0x40, 0x05, 0x8d, 0xeb, 0xff, 0xff, 0x3c, 0x51, 0x97, 0x7f, 0x24, 0x56, 0x2e,
+                    0x21);
+    private static final byte[] AEM35 = asBytes(0xb2, 0xad, 0xd3, 0xb7);
+    private static final byte[] RPI36 =
+            asBytes(
+                    0x6a, 0x68, 0xe3, 0x0b, 0x2f, 0xb9, 0x3b, 0x5d, 0xf7, 0x8e, 0xe3, 0xa9, 0xa3, 0x50, 0xa6,
+                    0xce);
+    private static final byte[] AEM36 = asBytes(0x1b, 0xfb, 0x46, 0x0a);
+    private static final byte[] RPI37 =
+            asBytes(
+                    0x8d, 0x33, 0xa7, 0x05, 0x62, 0x62, 0x99, 0x94, 0xf8, 0xdf, 0x99, 0x05, 0x2b, 0x0e, 0xb6,
+                    0x9a);
+    private static final byte[] AEM37 = asBytes(0x8e, 0xcf, 0x07, 0x7b);
+    private static final byte[] RPI38 =
+            asBytes(
+                    0x21, 0x05, 0x3c, 0xcb, 0x8f, 0x92, 0x51, 0x11, 0xe2, 0x54, 0xbd, 0x69, 0x4e, 0x97, 0x94,
+                    0x6b);
+    private static final byte[] AEM38 = asBytes(0x2b, 0xa7, 0x7b, 0x38);
+    private static final byte[] RPI39 =
+            asBytes(
+                    0xe1, 0xc9, 0xcd, 0xf2, 0x0f, 0x90, 0x0a, 0xe6, 0xd2, 0x4b, 0xf7, 0xbc, 0xb4, 0xe6, 0x61,
+                    0x35);
+    private static final byte[] AEM39 = asBytes(0x8c, 0x12, 0x40, 0xde);
+    private static final byte[] RPI40 =
+            asBytes(
+                    0xba, 0xf7, 0x89, 0xf5, 0x50, 0x94, 0x6b, 0x43, 0x10, 0x64, 0x45, 0x07, 0x71, 0xb2, 0xa1,
+                    0x43);
+    private static final byte[] AEM40 = asBytes(0x43, 0x89, 0x99, 0xb6);
+    private static final byte[] RPI41 =
+            asBytes(
+                    0xf7, 0xf9, 0x1e, 0xc2, 0x50, 0x85, 0xd0, 0x35, 0x3e, 0x02, 0x78, 0xe5, 0x98, 0xcc, 0x62,
+                    0x01);
+    private static final byte[] AEM41 = asBytes(0x1d, 0xcb, 0xdc, 0x6e);
+    private static final byte[] RPI42 =
+            asBytes(
+                    0xc8, 0x93, 0x3d, 0x70, 0x22, 0x0b, 0xa9, 0xc8, 0xc1, 0x48, 0x39, 0x3a, 0x39, 0x59, 0xd2,
+                    0x56);
+    private static final byte[] AEM42 = asBytes(0x63, 0x85, 0xff, 0xda);
+    private static final byte[] RPI43 =
+            asBytes(
+                    0xf2, 0x93, 0x2e, 0x6e, 0x6e, 0xf6, 0x0f, 0x0f, 0x5b, 0xbc, 0xe4, 0x39, 0x10, 0x0a, 0x90,
+                    0xe4);
+    private static final byte[] AEM43 = asBytes(0x69, 0x6c, 0x0c, 0xdf);
+    private static final byte[] RPI44 =
+            asBytes(
+                    0x18, 0xf5, 0xd8, 0x10, 0x2a, 0x59, 0x30, 0xd8, 0x02, 0x30, 0xf2, 0xc3, 0x9a, 0x42, 0x66,
+                    0xd6);
+    private static final byte[] AEM44 = asBytes(0x63, 0x84, 0x2b, 0xba);
+    private static final byte[] RPI45 =
+            asBytes(
+                    0xe6, 0x07, 0x5c, 0x28, 0x93, 0x9f, 0xb0, 0xc6, 0x72, 0x46, 0xce, 0x38, 0xc5, 0xff, 0x93,
+                    0x8a);
+    private static final byte[] AEM45 = asBytes(0xb7, 0xe2, 0x2c, 0x60);
+    private static final byte[] RPI46 =
+            asBytes(
+                    0x66, 0x16, 0x88, 0x62, 0xbc, 0x44, 0x5f, 0x48, 0xe5, 0xb0, 0xed, 0x07, 0xe1, 0xdf, 0x3f,
+                    0x5a);
+    private static final byte[] AEM46 = asBytes(0xd2, 0xe7, 0xd6, 0xd8);
+    private static final byte[] RPI47 =
+            asBytes(
+                    0x1d, 0x0a, 0x01, 0xc3, 0x8d, 0xa4, 0xac, 0x41, 0xec, 0x7a, 0x63, 0x8f, 0x5d, 0xf7, 0x05,
+                    0xa9);
+    private static final byte[] AEM47 = asBytes(0x91, 0xbe, 0x92, 0x2c);
+    private static final byte[] RPI48 =
+            asBytes(
+                    0xa7, 0x37, 0x00, 0x1a, 0x2d, 0x2f, 0x80, 0x2c, 0x64, 0x78, 0x9a, 0x99, 0x52, 0xe6, 0xd1,
+                    0xa7);
+    private static final byte[] AEM48 = asBytes(0x7e, 0x04, 0x21, 0xbb);
+    private static final byte[] RPI49 =
+            asBytes(
+                    0x7c, 0x37, 0x25, 0xb6, 0x08, 0x4e, 0x68, 0x1f, 0xb3, 0x4d, 0x26, 0xc3, 0xa3, 0x94, 0xa6,
+                    0x43);
+    private static final byte[] AEM49 = asBytes(0x7e, 0xa5, 0x20, 0x9f);
+    private static final byte[] RPI50 =
+            asBytes(
+                    0x26, 0xd1, 0xf8, 0x36, 0x55, 0x7a, 0x25, 0x9a, 0x81, 0xb5, 0xdb, 0x54, 0x19, 0xc6, 0xa7,
+                    0x29);
+    private static final byte[] AEM50 = asBytes(0x1c, 0x92, 0x06, 0x28);
+    private static final byte[] RPI51 =
+            asBytes(
+                    0xeb, 0xc2, 0xa6, 0x06, 0x28, 0x54, 0xd1, 0xec, 0x62, 0x7b, 0x1f, 0x6e, 0x84, 0x32, 0xe1,
+                    0x66);
+    private static final byte[] AEM51 = asBytes(0x4a, 0x76, 0x46, 0x32);
+    private static final byte[] RPI52 =
+            asBytes(
+                    0x11, 0x32, 0x74, 0xe8, 0x0c, 0x31, 0xcf, 0xcd, 0x81, 0xc2, 0xad, 0x08, 0x64, 0x44, 0x51,
+                    0x78);
+    private static final byte[] AEM52 = asBytes(0x69, 0xa7, 0x49, 0xb6);
+    private static final byte[] RPI53 =
+            asBytes(
+                    0x45, 0x67, 0x97, 0x6c, 0x48, 0xbe, 0x72, 0x59, 0x06, 0x24, 0x7d, 0x0b, 0xd8, 0x1b, 0xb8,
+                    0x11);
+    private static final byte[] AEM53 = asBytes(0x6f, 0x94, 0x85, 0xdb);
+    private static final byte[] RPI54 =
+            asBytes(
+                    0x74, 0x81, 0x54, 0xba, 0x52, 0x3a, 0x1a, 0xa8, 0x10, 0xb7, 0x06, 0x2a, 0x13, 0xe5, 0xaa,
+                    0x68);
+    private static final byte[] AEM54 = asBytes(0x74, 0x78, 0x07, 0x23);
+    private static final byte[] RPI55 =
+            asBytes(
+                    0x30, 0xbc, 0xeb, 0x33, 0x45, 0x74, 0x51, 0x53, 0x35, 0x23, 0x65, 0x99, 0x85, 0x87, 0xcd,
+                    0x10);
+    private static final byte[] AEM55 = asBytes(0x89, 0x17, 0xda, 0x61);
+    private static final byte[] RPI56 =
+            asBytes(
+                    0x8f, 0x2d, 0x7b, 0x87, 0x00, 0xa8, 0x2f, 0xd4, 0x51, 0x4d, 0xfa, 0x42, 0x02, 0xee, 0x29,
+                    0x8f);
+    private static final byte[] AEM56 = asBytes(0xfe, 0xc4, 0xf8, 0xb1);
+    private static final byte[] RPI57 =
+            asBytes(
+                    0x0e, 0x66, 0x49, 0x53, 0x70, 0x0c, 0xdf, 0xc0, 0xd2, 0x79, 0x2f, 0xad, 0xf0, 0x73, 0x29,
+                    0xeb);
+    private static final byte[] AEM57 = asBytes(0x78, 0x1a, 0x3e, 0xaf);
+    private static final byte[] RPI58 =
+            asBytes(
+                    0xf4, 0x49, 0x58, 0xc4, 0xdd, 0x70, 0xd9, 0x96, 0x8a, 0x26, 0xfd, 0x60, 0xba, 0x92, 0x72,
+                    0x90);
+    private static final byte[] AEM58 = asBytes(0xb9, 0x75, 0x9b, 0x61);
+    private static final byte[] RPI59 =
+            asBytes(
+                    0x55, 0xfd, 0x2f, 0x6c, 0xbd, 0xe0, 0xe1, 0x3f, 0xd2, 0x2c, 0x0b, 0x3d, 0xb1, 0x62, 0x28,
+                    0xe5);
+    private static final byte[] AEM59 = asBytes(0x48, 0x3c, 0x94, 0x10);
+    private static final byte[] RPI60 =
+            asBytes(
+                    0x49, 0xf3, 0xf9, 0xd1, 0x24, 0x69, 0xdc, 0xc9, 0xed, 0x35, 0x63, 0x64, 0xc3, 0x00, 0x66,
+                    0xe4);
+    private static final byte[] AEM60 = asBytes(0x2d, 0xa6, 0xeb, 0xac);
+    private static final byte[] RPI61 =
+            asBytes(
+                    0xc5, 0x7d, 0x2d, 0x6e, 0x0d, 0x25, 0xa0, 0x65, 0x1c, 0xd7, 0x27, 0x86, 0xf8, 0xc9, 0x51,
+                    0xce);
+    private static final byte[] AEM61 = asBytes(0x43, 0xea, 0xcf, 0x34);
+    private static final byte[] RPI62 =
+            asBytes(
+                    0x88, 0xef, 0x25, 0x63, 0x51, 0xac, 0x49, 0xdf, 0xd1, 0x5a, 0xb5, 0xa2, 0xde, 0x97, 0xc0,
+                    0x13);
+    private static final byte[] AEM62 = asBytes(0xb9, 0xa1, 0x36, 0x53);
+    private static final byte[] RPI63 =
+            asBytes(
+                    0xd0, 0xfb, 0x6f, 0xd6, 0xdb, 0x89, 0xda, 0x52, 0x36, 0x1f, 0x1a, 0x30, 0xfb, 0x43, 0x6c,
+                    0xe7);
+    private static final byte[] AEM63 = asBytes(0x35, 0x6d, 0xea, 0x55);
+    private static final byte[] RPI64 =
+            asBytes(
+                    0x8a, 0x42, 0xa3, 0x30, 0xf0, 0x19, 0x28, 0xe5, 0x16, 0x31, 0x23, 0x19, 0x81, 0x60, 0x3f,
+                    0xd5);
+    private static final byte[] AEM64 = asBytes(0x35, 0x05, 0xb7, 0xf3);
+    private static final byte[] RPI65 =
+            asBytes(
+                    0x6e, 0x51, 0xb2, 0xa2, 0xae, 0xcb, 0xab, 0x1d, 0xf8, 0x08, 0x26, 0xef, 0x6d, 0x1e, 0x19,
+                    0x58);
+    private static final byte[] AEM65 = asBytes(0x5c, 0x05, 0xcd, 0x94);
+    private static final byte[] RPI66 =
+            asBytes(
+                    0x3c, 0xe6, 0x81, 0xa2, 0x8b, 0x1b, 0xe1, 0x9c, 0x9e, 0x36, 0xb9, 0xc5, 0x80, 0xb1, 0x23,
+                    0xab);
+    private static final byte[] AEM66 = asBytes(0x98, 0x47, 0x45, 0xd6);
+    private static final byte[] RPI67 =
+            asBytes(
+                    0x1e, 0x95, 0x8e, 0xd8, 0x9b, 0x86, 0xb9, 0x89, 0x77, 0xf7, 0x9e, 0x1b, 0x83, 0xf3, 0xd0,
+                    0x5f);
+    private static final byte[] AEM67 = asBytes(0x93, 0xfd, 0xf7, 0x08);
+    private static final byte[] RPI68 =
+            asBytes(
+                    0x1d, 0x66, 0x7b, 0x01, 0xd6, 0x63, 0x4b, 0x5f, 0x3b, 0x6f, 0x33, 0xac, 0x4b, 0x15, 0x0d,
+                    0x23);
+    private static final byte[] AEM68 = asBytes(0xd7, 0x0f, 0x74, 0x4c);
+    private static final byte[] RPI69 =
+            asBytes(
+                    0x67, 0xf2, 0x22, 0x15, 0x6c, 0x51, 0x7d, 0xeb, 0xc0, 0x70, 0x68, 0xcb, 0xc5, 0xee, 0xc1,
+                    0xdd);
+    private static final byte[] AEM69 = asBytes(0x60, 0x57, 0x8c, 0x31);
+    private static final byte[] RPI70 =
+            asBytes(
+                    0xa8, 0x45, 0x4b, 0x9c, 0x94, 0x7d, 0x16, 0x25, 0xee, 0x3f, 0xba, 0x26, 0x07, 0xc2, 0x3a,
+                    0xff);
+    private static final byte[] AEM70 = asBytes(0x79, 0x00, 0xee, 0xad);
+    private static final byte[] RPI71 =
+            asBytes(
+                    0x41, 0x6f, 0xfc, 0x7a, 0x32, 0xfc, 0xfd, 0xa9, 0xa3, 0x16, 0xd0, 0x17, 0x90, 0xe3, 0x19,
+                    0x45);
+    private static final byte[] AEM71 = asBytes(0xc1, 0x22, 0xad, 0x68);
+    private static final byte[] RPI72 =
+            asBytes(
+                    0xc1, 0x9a, 0x30, 0xc3, 0x9c, 0x9c, 0x3a, 0x08, 0x9b, 0xca, 0xdd, 0xe1, 0xc6, 0x69, 0x94,
+                    0x47);
+    private static final byte[] AEM72 = asBytes(0x34, 0x0a, 0xa3, 0x82);
+    private static final byte[] RPI73 =
+            asBytes(
+                    0x78, 0x6d, 0xdf, 0xae, 0x6f, 0xc7, 0x7c, 0x4c, 0x41, 0x0c, 0x4e, 0xc3, 0x2d, 0x34, 0x24,
+                    0x7d);
+    private static final byte[] AEM73 = asBytes(0x67, 0xc6, 0x0d, 0xfb);
+    private static final byte[] RPI74 =
+            asBytes(
+                    0xef, 0x0f, 0xd3, 0xa9, 0x5b, 0x96, 0x61, 0xe1, 0xfc, 0xcb, 0x4e, 0x30, 0xcd, 0xe3, 0x2c,
+                    0x51);
+    private static final byte[] AEM74 = asBytes(0x45, 0x56, 0xb6, 0x73);
+    private static final byte[] RPI75 =
+            asBytes(
+                    0xfc, 0x1f, 0x8a, 0x66, 0xf4, 0x05, 0xcc, 0xb6, 0x3d, 0xc3, 0xe4, 0x82, 0x07, 0xda, 0x77,
+                    0x88);
+    private static final byte[] AEM75 = asBytes(0x9e, 0x4f, 0x1d, 0xb4);
+    private static final byte[] RPI76 =
+            asBytes(
+                    0x0e, 0xac, 0xc2, 0x86, 0x31, 0xb1, 0x0f, 0x44, 0x98, 0x36, 0x86, 0x66, 0x13, 0x0f, 0xf0,
+                    0xc9);
+    private static final byte[] AEM76 = asBytes(0x5b, 0xe0, 0x4e, 0x9d);
+    private static final byte[] RPI77 =
+            asBytes(
+                    0xe8, 0xdb, 0x4a, 0x46, 0x26, 0x38, 0x5a, 0xe6, 0xe3, 0xb2, 0x45, 0x1d, 0x0a, 0x66, 0xed,
+                    0xbf);
+    private static final byte[] AEM77 = asBytes(0x80, 0x6a, 0xf2, 0xf9);
+    private static final byte[] RPI78 =
+            asBytes(
+                    0xc6, 0x00, 0x67, 0x8d, 0x4f, 0xbe, 0x92, 0x45, 0xad, 0x49, 0x73, 0xb1, 0xc8, 0x97, 0x1b,
+                    0xc5);
+    private static final byte[] AEM78 = asBytes(0x7a, 0xf8, 0xd4, 0xfd);
+    private static final byte[] RPI79 =
+            asBytes(
+                    0x08, 0x19, 0x26, 0xc3, 0x61, 0x83, 0x4c, 0x5c, 0x1d, 0x43, 0x19, 0xb8, 0x40, 0xf3, 0x15,
+                    0xae);
+    private static final byte[] AEM79 = asBytes(0xfc, 0x8d, 0xdd, 0xd0);
+    private static final byte[] RPI80 =
+            asBytes(
+                    0x1d, 0x82, 0x9e, 0xaf, 0xa0, 0x42, 0x32, 0xa6, 0xbb, 0x4d, 0x3c, 0x20, 0x22, 0xac, 0x3d,
+                    0x0f);
+    private static final byte[] AEM80 = asBytes(0xda, 0x88, 0x15, 0x68);
+    private static final byte[] RPI81 =
+            asBytes(
+                    0x79, 0x8a, 0xbc, 0xe8, 0xc8, 0xf6, 0x25, 0x10, 0xde, 0x59, 0x5a, 0x99, 0x73, 0xfb, 0x21,
+                    0x8e);
+    private static final byte[] AEM81 = asBytes(0x13, 0x95, 0xcf, 0x7b);
+    private static final byte[] RPI82 =
+            asBytes(
+                    0x61, 0xfc, 0x0d, 0xeb, 0x47, 0x08, 0x4f, 0xda, 0xf1, 0x48, 0x3e, 0x34, 0x2d, 0x73, 0xb1,
+                    0x48);
+    private static final byte[] AEM82 = asBytes(0xd9, 0xe2, 0x5d, 0xb6);
+    private static final byte[] RPI83 =
+            asBytes(
+                    0x49, 0x69, 0xed, 0xd4, 0x0f, 0x3e, 0xab, 0x46, 0x8b, 0x8a, 0x8d, 0x49, 0x68, 0x5a, 0x4e,
+                    0xdd);
+    private static final byte[] AEM83 = asBytes(0xc1, 0xb2, 0x05, 0x97);
+    private static final byte[] RPI84 =
+            asBytes(
+                    0x1d, 0x69, 0xd6, 0xd4, 0x15, 0x8a, 0x31, 0x8f, 0x1d, 0x9c, 0xaf, 0xba, 0x13, 0x58, 0x70,
+                    0x17);
+    private static final byte[] AEM84 = asBytes(0xeb, 0x53, 0x91, 0x99);
+    private static final byte[] RPI85 =
+            asBytes(
+                    0xab, 0x7c, 0x61, 0xf1, 0xcc, 0xa8, 0x13, 0xfd, 0x36, 0xe8, 0xf1, 0xb1, 0xe5, 0xdf, 0x6a,
+                    0x0f);
+    private static final byte[] AEM85 = asBytes(0xc8, 0x1a, 0xb8, 0x54);
+    private static final byte[] RPI86 =
+            asBytes(
+                    0x94, 0xf2, 0x4f, 0xde, 0xc1, 0x7a, 0xa3, 0x1f, 0x74, 0xf2, 0x02, 0xae, 0x4a, 0x68, 0x74,
+                    0xbe);
+    private static final byte[] AEM86 = asBytes(0x46, 0xee, 0x82, 0xdc);
+    private static final byte[] RPI87 =
+            asBytes(
+                    0x6e, 0x5d, 0xe0, 0x25, 0x79, 0xd3, 0xdf, 0xb1, 0x94, 0xcc, 0x7b, 0xd4, 0x92, 0x70, 0x25,
+                    0x3d);
+    private static final byte[] AEM87 = asBytes(0x8a, 0x05, 0x28, 0xbd);
+    private static final byte[] RPI88 =
+            asBytes(
+                    0xfe, 0x3e, 0x1e, 0x36, 0x21, 0x77, 0x3f, 0x18, 0x80, 0x40, 0xaa, 0x5d, 0xb3, 0xff, 0x1d,
+                    0x4e);
+    private static final byte[] AEM88 = asBytes(0x28, 0x03, 0x3a, 0xae);
+    private static final byte[] RPI89 =
+            asBytes(
+                    0xd7, 0x37, 0x6e, 0x0d, 0x77, 0x25, 0x5d, 0xe2, 0x3d, 0x54, 0x0f, 0x02, 0x71, 0x83, 0xf1,
+                    0xba);
+    private static final byte[] AEM89 = asBytes(0xa0, 0x1c, 0xe7, 0x12);
+    private static final byte[] RPI90 =
+            asBytes(
+                    0x64, 0xa7, 0x1e, 0x48, 0xa5, 0x0e, 0x7b, 0x5a, 0x37, 0xac, 0x91, 0x81, 0x6e, 0x2b, 0x0f,
+                    0x53);
+    private static final byte[] AEM90 = asBytes(0xc9, 0xcb, 0x8c, 0x70);
+    private static final byte[] RPI91 =
+            asBytes(
+                    0x0f, 0x22, 0xa2, 0xc0, 0xb6, 0x99, 0xe1, 0x89, 0xd5, 0x9e, 0x30, 0xd1, 0x74, 0x5d, 0x67,
+                    0xd3);
+    private static final byte[] AEM91 = asBytes(0xdb, 0xe6, 0x8f, 0x47);
+    private static final byte[] RPI92 =
+            asBytes(
+                    0xde, 0x87, 0x6b, 0xaf, 0x31, 0x22, 0x8e, 0x3b, 0x7f, 0xe0, 0xf0, 0x8e, 0x1f, 0x38, 0xea,
+                    0x7b);
+    private static final byte[] AEM92 = asBytes(0x4c, 0xb1, 0xd9, 0x4f);
+    private static final byte[] RPI93 =
+            asBytes(
+                    0x8c, 0x69, 0x27, 0xbc, 0xf5, 0xf7, 0xae, 0xe1, 0xee, 0xd8, 0xab, 0xbe, 0x43, 0xe2, 0xe2,
+                    0xd1);
+    private static final byte[] AEM93 = asBytes(0x27, 0x74, 0x06, 0x32);
+    private static final byte[] RPI94 =
+            asBytes(
+                    0xbc, 0x96, 0x83, 0x0f, 0x18, 0x2a, 0x72, 0xc8, 0x9e, 0x65, 0xce, 0xa9, 0xc4, 0x7d, 0x88,
+                    0xc0);
+    private static final byte[] AEM94 = asBytes(0xe8, 0xad, 0xeb, 0x6d);
+    private static final byte[] RPI95 =
+            asBytes(
+                    0x7b, 0x2f, 0xbe, 0x74, 0x6d, 0xd2, 0xda, 0x86, 0xe9, 0x86, 0x6a, 0x0e, 0x6a, 0xad, 0xbc,
+                    0x4d);
+    private static final byte[] AEM95 = asBytes(0xe5, 0x3a, 0x5a, 0xc7);
+    private static final byte[] RPI96 =
+            asBytes(
+                    0x93, 0xe2, 0x0f, 0x14, 0xa1, 0x3d, 0x56, 0x56, 0x75, 0x94, 0xaa, 0x96, 0x23, 0x50, 0xf1,
+                    0x70);
+    private static final byte[] AEM96 = asBytes(0x94, 0x32, 0x42, 0xa4);
+    private static final byte[] RPI97 =
+            asBytes(
+                    0x9e, 0x38, 0x14, 0xf9, 0x51, 0xfa, 0x03, 0x79, 0x6b, 0x9a, 0x66, 0xf8, 0x9a, 0x9f, 0x40,
+                    0x0d);
+    private static final byte[] AEM97 = asBytes(0x06, 0x48, 0xdc, 0x89);
+    private static final byte[] RPI98 =
+            asBytes(
+                    0x95, 0xfa, 0x09, 0x84, 0x5a, 0xa8, 0xd4, 0xb6, 0x00, 0x47, 0xfa, 0xf9, 0x9a, 0xeb, 0xca,
+                    0x0c);
+    private static final byte[] AEM98 = asBytes(0x85, 0x5e, 0x31, 0xb3);
+    private static final byte[] RPI99 =
+            asBytes(
+                    0xef, 0x0a, 0x75, 0x79, 0x33, 0x18, 0x53, 0xb9, 0xeb, 0xc2, 0x50, 0xb4, 0xd6, 0xf3, 0xeb,
+                    0xcc);
+    private static final byte[] AEM99 = asBytes(0x9c, 0x1f, 0x07, 0xc2);
+    private static final byte[] RPI100 =
+            asBytes(
+                    0x7f, 0xe9, 0xa9, 0x0d, 0xe0, 0x0c, 0x9f, 0x07, 0x37, 0xd3, 0xb4, 0x5f, 0xda, 0x65, 0x11,
+                    0x15);
+    private static final byte[] AEM100 = asBytes(0x98, 0x4e, 0x1f, 0xf3);
+    private static final byte[] RPI101 =
+            asBytes(
+                    0x44, 0x3b, 0x7b, 0x5a, 0xb9, 0xa8, 0x6a, 0x1f, 0xee, 0x67, 0xe1, 0x8c, 0xb8, 0xc4, 0x07,
+                    0x64);
+    private static final byte[] AEM101 = asBytes(0xb3, 0xfb, 0xa7, 0xe1);
+    private static final byte[] RPI102 =
+            asBytes(
+                    0xe8, 0xa6, 0xfa, 0x9a, 0x5c, 0xa9, 0xfb, 0x06, 0x1c, 0x4c, 0xdb, 0xe2, 0x17, 0xc6, 0x1d,
+                    0x59);
+    private static final byte[] AEM102 = asBytes(0x3b, 0x6d, 0x9d, 0xe8);
+    private static final byte[] RPI103 =
+            asBytes(
+                    0x08, 0x7a, 0x08, 0x04, 0x06, 0x86, 0xfd, 0x63, 0x5e, 0xf3, 0x89, 0x75, 0x27, 0x41, 0xcc,
+                    0x1f);
+    private static final byte[] AEM103 = asBytes(0xf0, 0x85, 0x55, 0x1c);
+    private static final byte[] RPI104 =
+            asBytes(
+                    0x58, 0x7c, 0x04, 0x86, 0x64, 0x4c, 0xeb, 0x2d, 0x0b, 0x7e, 0xbd, 0xd3, 0x9d, 0xd3, 0xa8,
+                    0x60);
+    private static final byte[] AEM104 = asBytes(0x0a, 0x6b, 0x32, 0x6e);
+    private static final byte[] RPI105 =
+            asBytes(
+                    0xdd, 0x82, 0x7b, 0xa6, 0x0f, 0x8a, 0x35, 0xb1, 0xdd, 0x4e, 0x4c, 0xdf, 0xe4, 0x9c, 0x42,
+                    0x63);
+    private static final byte[] AEM105 = asBytes(0x81, 0xeb, 0x20, 0xe2);
+    private static final byte[] RPI106 =
+            asBytes(
+                    0xcf, 0x23, 0x40, 0x00, 0x08, 0x0a, 0x4e, 0x8d, 0xa8, 0xfe, 0xb5, 0x33, 0xaa, 0x59, 0x04,
+                    0xd3);
+    private static final byte[] AEM106 = asBytes(0x34, 0xd8, 0x6e, 0xe5);
+    private static final byte[] RPI107 =
+            asBytes(
+                    0xc5, 0x0f, 0xb1, 0xec, 0x3e, 0xf5, 0x4e, 0x91, 0x61, 0x78, 0xca, 0x9d, 0x56, 0xee, 0x4f,
+                    0x5c);
+    private static final byte[] AEM107 = asBytes(0xea, 0xea, 0xc3, 0xd2);
+    private static final byte[] RPI108 =
+            asBytes(
+                    0xd4, 0x5f, 0xde, 0x46, 0xf4, 0x67, 0xd7, 0x6e, 0xd2, 0x8d, 0xd4, 0xd2, 0x49, 0x6d, 0xcb,
+                    0x7f);
+    private static final byte[] AEM108 = asBytes(0xea, 0xa6, 0x4a, 0x8e);
+    private static final byte[] RPI109 =
+            asBytes(
+                    0xe9, 0xba, 0xf8, 0x1c, 0x96, 0x1f, 0x51, 0x0d, 0x08, 0xa6, 0x63, 0xc7, 0x52, 0x4f, 0x36,
+                    0xdf);
+    private static final byte[] AEM109 = asBytes(0x1e, 0x2c, 0xa3, 0x7b);
+    private static final byte[] RPI110 =
+            asBytes(
+                    0xe3, 0xb0, 0x2c, 0xc0, 0x31, 0xe1, 0x44, 0xfa, 0xe6, 0x1a, 0x38, 0x99, 0x50, 0x1b, 0x49,
+                    0x21);
+    private static final byte[] AEM110 = asBytes(0x44, 0xc0, 0xd7, 0x06);
+    private static final byte[] RPI111 =
+            asBytes(
+                    0xa7, 0x5e, 0xea, 0x58, 0x23, 0x2e, 0x73, 0x66, 0xce, 0xa1, 0xa0, 0xe7, 0x2d, 0xa0, 0xce,
+                    0xc5);
+    private static final byte[] AEM111 = asBytes(0x5a, 0x8c, 0x79, 0xb7);
+    private static final byte[] RPI112 =
+            asBytes(
+                    0x6a, 0xd7, 0x62, 0x1f, 0xe1, 0xda, 0x01, 0x39, 0xff, 0x8b, 0xad, 0x7f, 0x37, 0x9c, 0xab,
+                    0xf6);
+    private static final byte[] AEM112 = asBytes(0x27, 0x9f, 0x16, 0xb1);
+    private static final byte[] RPI113 =
+            asBytes(
+                    0x6f, 0xe2, 0xac, 0x45, 0xf6, 0x5c, 0x8a, 0xc6, 0x9f, 0xdc, 0x5e, 0xf7, 0xfa, 0x9f, 0xf7,
+                    0xf0);
+    private static final byte[] AEM113 = asBytes(0x4b, 0xd9, 0x07, 0xaa);
+    private static final byte[] RPI114 =
+            asBytes(
+                    0x2f, 0xbe, 0xc6, 0x8f, 0xd2, 0x7d, 0xdd, 0xdb, 0x42, 0x23, 0x04, 0x4e, 0xfc, 0x77, 0x98,
+                    0x51);
+    private static final byte[] AEM114 = asBytes(0x2d, 0xf6, 0xc5, 0xeb);
+    private static final byte[] RPI115 =
+            asBytes(
+                    0x32, 0xbf, 0x68, 0x8a, 0x7c, 0x83, 0x4b, 0xe1, 0xbf, 0xab, 0x7c, 0x8e, 0x0e, 0x58, 0x0a,
+                    0xdb);
+    private static final byte[] AEM115 = asBytes(0x66, 0x85, 0x2d, 0x43);
+    private static final byte[] RPI116 =
+            asBytes(
+                    0xda, 0xe3, 0xa7, 0xd8, 0xc6, 0x24, 0x27, 0xb0, 0x9c, 0x0e, 0x7b, 0xbf, 0x48, 0x9d, 0x34,
+                    0xbd);
+    private static final byte[] AEM116 = asBytes(0x83, 0x6d, 0x3b, 0x95);
+    private static final byte[] RPI117 =
+            asBytes(
+                    0x3c, 0x4b, 0x02, 0xbd, 0x5e, 0xd2, 0x8c, 0x67, 0x82, 0x9c, 0x97, 0x79, 0x10, 0x79, 0xaf,
+                    0xd2);
+    private static final byte[] AEM117 = asBytes(0x27, 0x35, 0xb9, 0x97);
+    private static final byte[] RPI118 =
+            asBytes(
+                    0xe2, 0xfa, 0xea, 0xc3, 0xdb, 0xd1, 0x50, 0xec, 0x8e, 0xa8, 0xe7, 0xb3, 0xe5, 0xbb, 0x84,
+                    0x54);
+    private static final byte[] AEM118 = asBytes(0xd7, 0x1f, 0x97, 0xc2);
+    private static final byte[] RPI119 =
+            asBytes(
+                    0x69, 0x94, 0x2a, 0x72, 0x13, 0xea, 0xf3, 0xc1, 0x4a, 0x69, 0x99, 0x6b, 0xa6, 0xc6, 0xbf,
+                    0xeb);
+    private static final byte[] AEM119 = asBytes(0x53, 0xbc, 0x4d, 0xb5);
+    private static final byte[] RPI120 =
+            asBytes(
+                    0x1c, 0x5c, 0x4d, 0xd2, 0x54, 0x52, 0xe9, 0x7d, 0xd1, 0x87, 0xdd, 0x7c, 0xe1, 0xd1, 0xee,
+                    0x81);
+    private static final byte[] AEM120 = asBytes(0x48, 0xa4, 0xd3, 0x79);
+    private static final byte[] RPI121 =
+            asBytes(
+                    0xfb, 0xf5, 0x60, 0x7a, 0x7c, 0x61, 0x2a, 0xce, 0xd1, 0x60, 0xe7, 0x55, 0xa9, 0x87, 0x26,
+                    0x2d);
+    private static final byte[] AEM121 = asBytes(0xb7, 0x8d, 0xc1, 0xf5);
+    private static final byte[] RPI122 =
+            asBytes(
+                    0x3e, 0x2d, 0xe1, 0x30, 0x70, 0xf2, 0x74, 0x43, 0xd9, 0xba, 0x3e, 0xb4, 0x3f, 0x9a, 0x71,
+                    0xea);
+    private static final byte[] AEM122 = asBytes(0x58, 0x21, 0x70, 0xca);
+    private static final byte[] RPI123 =
+            asBytes(
+                    0x8a, 0x12, 0xd2, 0x5f, 0x00, 0x6f, 0xab, 0x5a, 0x27, 0x07, 0xda, 0x9e, 0x6c, 0x4e, 0x96,
+                    0xbe);
+    private static final byte[] AEM123 = asBytes(0x93, 0x95, 0x94, 0xcc);
+    private static final byte[] RPI124 =
+            asBytes(
+                    0x6f, 0xd9, 0x8c, 0x22, 0xe2, 0x27, 0x83, 0x8e, 0x6f, 0x67, 0x36, 0x97, 0x64, 0x43, 0x77,
+                    0x25);
+    private static final byte[] AEM124 = asBytes(0xc1, 0x4f, 0x5b, 0x11);
+    private static final byte[] RPI125 =
+            asBytes(
+                    0x3d, 0xa2, 0x12, 0xae, 0xbd, 0xb7, 0x8b, 0xa8, 0x19, 0x80, 0x9d, 0x03, 0xc6, 0xcf, 0x56,
+                    0xe2);
+    private static final byte[] AEM125 = asBytes(0x30, 0x09, 0x12, 0xda);
+    private static final byte[] RPI126 =
+            asBytes(
+                    0x8c, 0x48, 0xda, 0x73, 0xe2, 0x9e, 0xff, 0xc9, 0xb7, 0x4b, 0xb0, 0x97, 0x09, 0x6e, 0x0a,
+                    0x0a);
+    private static final byte[] AEM126 = asBytes(0xce, 0x79, 0xc5, 0x0a);
+    private static final byte[] RPI127 =
+            asBytes(
+                    0xe5, 0x3c, 0x68, 0xb4, 0xb0, 0x1c, 0x68, 0xf3, 0x7e, 0x65, 0xa0, 0xdc, 0x8e, 0x67, 0xf4,
+                    0x5d);
+    private static final byte[] AEM127 = asBytes(0xe0, 0x4f, 0x38, 0x67);
+    private static final byte[] RPI128 =
+            asBytes(
+                    0x83, 0x31, 0xdd, 0xe6, 0x36, 0x4b, 0x11, 0x95, 0x27, 0xaf, 0x76, 0xfe, 0xe1, 0x7a, 0xab,
+                    0xcf);
+    private static final byte[] AEM128 = asBytes(0x64, 0x18, 0x7b, 0xdf);
+    private static final byte[] RPI129 =
+            asBytes(
+                    0x8c, 0x14, 0x47, 0x1f, 0x55, 0x71, 0x92, 0x63, 0x96, 0xdd, 0xe6, 0xf7, 0xb7, 0xb3, 0x5b,
+                    0x56);
+    private static final byte[] AEM129 = asBytes(0xe8, 0x7c, 0x05, 0xfd);
+    private static final byte[] RPI130 =
+            asBytes(
+                    0x4e, 0xb6, 0xb2, 0xde, 0xb4, 0x0e, 0x5e, 0xc9, 0xbc, 0x39, 0x83, 0x81, 0x02, 0xa4, 0xf4,
+                    0xf9);
+    private static final byte[] AEM130 = asBytes(0x4e, 0x70, 0x83, 0x25);
+    private static final byte[] RPI131 =
+            asBytes(
+                    0x77, 0xf2, 0x14, 0x1c, 0xef, 0xfd, 0x0a, 0xa3, 0xbe, 0xe4, 0xb6, 0x7c, 0x45, 0x0d, 0x9a,
+                    0xa6);
+    private static final byte[] AEM131 = asBytes(0xa1, 0x57, 0xeb, 0x59);
+    private static final byte[] RPI132 =
+            asBytes(
+                    0x04, 0x3d, 0x78, 0xe2, 0x0c, 0xb5, 0x9c, 0x0b, 0xcb, 0x15, 0x78, 0xff, 0x93, 0xea, 0x54,
+                    0x4a);
+    private static final byte[] AEM132 = asBytes(0x8d, 0x30, 0x43, 0x2a);
+    private static final byte[] RPI133 =
+            asBytes(
+                    0x65, 0xb8, 0xec, 0xc4, 0x56, 0x1c, 0x1c, 0xca, 0x05, 0x3d, 0x81, 0x4f, 0xfd, 0x89, 0x61,
+                    0xd4);
+    private static final byte[] AEM133 = asBytes(0x5b, 0x24, 0x38, 0x90);
+    private static final byte[] RPI134 =
+            asBytes(
+                    0x32, 0xf2, 0x5a, 0x17, 0x24, 0xf2, 0xbd, 0xca, 0xd0, 0x5a, 0xbc, 0x14, 0x82, 0xe1, 0x32,
+                    0x9e);
+    private static final byte[] AEM134 = asBytes(0x74, 0x90, 0xd2, 0x11);
+    private static final byte[] RPI135 =
+            asBytes(
+                    0x20, 0x3b, 0xa3, 0xf3, 0xf7, 0x23, 0x02, 0x66, 0xb9, 0x93, 0xb3, 0xee, 0x7b, 0x2d, 0x86,
+                    0x08);
+    private static final byte[] AEM135 = asBytes(0x1f, 0x01, 0x9d, 0xf2);
+    private static final byte[] RPI136 =
+            asBytes(
+                    0xe5, 0xe7, 0xa4, 0x70, 0x69, 0x21, 0x6e, 0x1a, 0x88, 0x7b, 0x90, 0xef, 0x03, 0x94, 0xa3,
+                    0x5c);
+    private static final byte[] AEM136 = asBytes(0x99, 0xe6, 0x12, 0xb7);
+    private static final byte[] RPI137 =
+            asBytes(
+                    0x3f, 0xfc, 0x8b, 0xb9, 0x1d, 0xbc, 0xd8, 0xee, 0x92, 0x49, 0x48, 0xf5, 0x08, 0x0b, 0x19,
+                    0x0d);
+    private static final byte[] AEM137 = asBytes(0x26, 0x4a, 0x57, 0x7e);
+    private static final byte[] RPI138 =
+            asBytes(
+                    0x32, 0x37, 0x53, 0x91, 0x07, 0x7f, 0xbf, 0x76, 0x86, 0xba, 0xfa, 0x7d, 0xc1, 0x56, 0xbe,
+                    0x1c);
+    private static final byte[] AEM138 = asBytes(0xa8, 0x11, 0xba, 0x62);
+    private static final byte[] RPI139 =
+            asBytes(
+                    0xa8, 0x90, 0x49, 0x65, 0xae, 0xc5, 0xdd, 0xb6, 0x55, 0xe9, 0x70, 0x07, 0xd2, 0x23, 0xdb,
+                    0x48);
+    private static final byte[] AEM139 = asBytes(0x2b, 0x48, 0x33, 0x21);
+    private static final byte[] RPI140 =
+            asBytes(
+                    0x41, 0x5c, 0xf7, 0xaf, 0x1d, 0xc9, 0xfe, 0xac, 0xb3, 0x97, 0x84, 0x88, 0xf5, 0x04, 0x68,
+                    0x93);
+    private static final byte[] AEM140 = asBytes(0xe2, 0xef, 0x53, 0x7e);
+    private static final byte[] RPI141 =
+            asBytes(
+                    0x86, 0xf8, 0xdd, 0xc0, 0xc9, 0x3e, 0x53, 0xe3, 0xa5, 0x82, 0xe6, 0x1f, 0x01, 0xf7, 0xdf,
+                    0x2f);
+    private static final byte[] AEM141 = asBytes(0x71, 0x1d, 0xbf, 0x6d);
+    private static final byte[] RPI142 =
+            asBytes(
+                    0xa6, 0x5e, 0xf7, 0xba, 0x97, 0x52, 0xc4, 0x17, 0x3b, 0xa4, 0x8a, 0x33, 0x84, 0x9c, 0x5e,
+                    0x52);
+    private static final byte[] AEM142 = asBytes(0x59, 0x2d, 0xbc, 0xa8);
+    private static final byte[] RPI143 =
+            asBytes(
+                    0xf4, 0x31, 0xb6, 0x2e, 0xcf, 0x44, 0x31, 0x02, 0xce, 0x4e, 0xd0, 0x40, 0x7d, 0xe5, 0x4b,
+                    0xd4);
+    private static final byte[] AEM143 = asBytes(0x12, 0x15, 0xe5, 0x7e);
+    // ------------------------------------------------------------------------------
+
+    public static final ImmutableList<AdvertisedData> ADVERTISED_DATA =
+            ImmutableList.of(
+                    new AdvertisedData(RPI0, AEM0),
+                    new AdvertisedData(RPI1, AEM1),
+                    new AdvertisedData(RPI2, AEM2),
+                    new AdvertisedData(RPI3, AEM3),
+                    new AdvertisedData(RPI4, AEM4),
+                    new AdvertisedData(RPI5, AEM5),
+                    new AdvertisedData(RPI6, AEM6),
+                    new AdvertisedData(RPI7, AEM7),
+                    new AdvertisedData(RPI8, AEM8),
+                    new AdvertisedData(RPI9, AEM9),
+                    new AdvertisedData(RPI10, AEM10),
+                    new AdvertisedData(RPI11, AEM11),
+                    new AdvertisedData(RPI12, AEM12),
+                    new AdvertisedData(RPI13, AEM13),
+                    new AdvertisedData(RPI14, AEM14),
+                    new AdvertisedData(RPI15, AEM15),
+                    new AdvertisedData(RPI16, AEM16),
+                    new AdvertisedData(RPI17, AEM17),
+                    new AdvertisedData(RPI18, AEM18),
+                    new AdvertisedData(RPI19, AEM19),
+                    new AdvertisedData(RPI20, AEM20),
+                    new AdvertisedData(RPI21, AEM21),
+                    new AdvertisedData(RPI22, AEM22),
+                    new AdvertisedData(RPI23, AEM23),
+                    new AdvertisedData(RPI24, AEM24),
+                    new AdvertisedData(RPI25, AEM25),
+                    new AdvertisedData(RPI26, AEM26),
+                    new AdvertisedData(RPI27, AEM27),
+                    new AdvertisedData(RPI28, AEM28),
+                    new AdvertisedData(RPI29, AEM29),
+                    new AdvertisedData(RPI30, AEM30),
+                    new AdvertisedData(RPI31, AEM31),
+                    new AdvertisedData(RPI32, AEM32),
+                    new AdvertisedData(RPI33, AEM33),
+                    new AdvertisedData(RPI34, AEM34),
+                    new AdvertisedData(RPI35, AEM35),
+                    new AdvertisedData(RPI36, AEM36),
+                    new AdvertisedData(RPI37, AEM37),
+                    new AdvertisedData(RPI38, AEM38),
+                    new AdvertisedData(RPI39, AEM39),
+                    new AdvertisedData(RPI40, AEM40),
+                    new AdvertisedData(RPI41, AEM41),
+                    new AdvertisedData(RPI42, AEM42),
+                    new AdvertisedData(RPI43, AEM43),
+                    new AdvertisedData(RPI44, AEM44),
+                    new AdvertisedData(RPI45, AEM45),
+                    new AdvertisedData(RPI46, AEM46),
+                    new AdvertisedData(RPI47, AEM47),
+                    new AdvertisedData(RPI48, AEM48),
+                    new AdvertisedData(RPI49, AEM49),
+                    new AdvertisedData(RPI50, AEM50),
+                    new AdvertisedData(RPI51, AEM51),
+                    new AdvertisedData(RPI52, AEM52),
+                    new AdvertisedData(RPI53, AEM53),
+                    new AdvertisedData(RPI54, AEM54),
+                    new AdvertisedData(RPI55, AEM55),
+                    new AdvertisedData(RPI56, AEM56),
+                    new AdvertisedData(RPI57, AEM57),
+                    new AdvertisedData(RPI58, AEM58),
+                    new AdvertisedData(RPI59, AEM59),
+                    new AdvertisedData(RPI60, AEM60),
+                    new AdvertisedData(RPI61, AEM61),
+                    new AdvertisedData(RPI62, AEM62),
+                    new AdvertisedData(RPI63, AEM63),
+                    new AdvertisedData(RPI64, AEM64),
+                    new AdvertisedData(RPI65, AEM65),
+                    new AdvertisedData(RPI66, AEM66),
+                    new AdvertisedData(RPI67, AEM67),
+                    new AdvertisedData(RPI68, AEM68),
+                    new AdvertisedData(RPI69, AEM69),
+                    new AdvertisedData(RPI70, AEM70),
+                    new AdvertisedData(RPI71, AEM71),
+                    new AdvertisedData(RPI72, AEM72),
+                    new AdvertisedData(RPI73, AEM73),
+                    new AdvertisedData(RPI74, AEM74),
+                    new AdvertisedData(RPI75, AEM75),
+                    new AdvertisedData(RPI76, AEM76),
+                    new AdvertisedData(RPI77, AEM77),
+                    new AdvertisedData(RPI78, AEM78),
+                    new AdvertisedData(RPI79, AEM79),
+                    new AdvertisedData(RPI80, AEM80),
+                    new AdvertisedData(RPI81, AEM81),
+                    new AdvertisedData(RPI82, AEM82),
+                    new AdvertisedData(RPI83, AEM83),
+                    new AdvertisedData(RPI84, AEM84),
+                    new AdvertisedData(RPI85, AEM85),
+                    new AdvertisedData(RPI86, AEM86),
+                    new AdvertisedData(RPI87, AEM87),
+                    new AdvertisedData(RPI88, AEM88),
+                    new AdvertisedData(RPI89, AEM89),
+                    new AdvertisedData(RPI90, AEM90),
+                    new AdvertisedData(RPI91, AEM91),
+                    new AdvertisedData(RPI92, AEM92),
+                    new AdvertisedData(RPI93, AEM93),
+                    new AdvertisedData(RPI94, AEM94),
+                    new AdvertisedData(RPI95, AEM95),
+                    new AdvertisedData(RPI96, AEM96),
+                    new AdvertisedData(RPI97, AEM97),
+                    new AdvertisedData(RPI98, AEM98),
+                    new AdvertisedData(RPI99, AEM99),
+                    new AdvertisedData(RPI100, AEM100),
+                    new AdvertisedData(RPI101, AEM101),
+                    new AdvertisedData(RPI102, AEM102),
+                    new AdvertisedData(RPI103, AEM103),
+                    new AdvertisedData(RPI104, AEM104),
+                    new AdvertisedData(RPI105, AEM105),
+                    new AdvertisedData(RPI106, AEM106),
+                    new AdvertisedData(RPI107, AEM107),
+                    new AdvertisedData(RPI108, AEM108),
+                    new AdvertisedData(RPI109, AEM109),
+                    new AdvertisedData(RPI110, AEM110),
+                    new AdvertisedData(RPI111, AEM111),
+                    new AdvertisedData(RPI112, AEM112),
+                    new AdvertisedData(RPI113, AEM113),
+                    new AdvertisedData(RPI114, AEM114),
+                    new AdvertisedData(RPI115, AEM115),
+                    new AdvertisedData(RPI116, AEM116),
+                    new AdvertisedData(RPI117, AEM117),
+                    new AdvertisedData(RPI118, AEM118),
+                    new AdvertisedData(RPI119, AEM119),
+                    new AdvertisedData(RPI120, AEM120),
+                    new AdvertisedData(RPI121, AEM121),
+                    new AdvertisedData(RPI122, AEM122),
+                    new AdvertisedData(RPI123, AEM123),
+                    new AdvertisedData(RPI124, AEM124),
+                    new AdvertisedData(RPI125, AEM125),
+                    new AdvertisedData(RPI126, AEM126),
+                    new AdvertisedData(RPI127, AEM127),
+                    new AdvertisedData(RPI128, AEM128),
+                    new AdvertisedData(RPI129, AEM129),
+                    new AdvertisedData(RPI130, AEM130),
+                    new AdvertisedData(RPI131, AEM131),
+                    new AdvertisedData(RPI132, AEM132),
+                    new AdvertisedData(RPI133, AEM133),
+                    new AdvertisedData(RPI134, AEM134),
+                    new AdvertisedData(RPI135, AEM135),
+                    new AdvertisedData(RPI136, AEM136),
+                    new AdvertisedData(RPI137, AEM137),
+                    new AdvertisedData(RPI138, AEM138),
+                    new AdvertisedData(RPI139, AEM139),
+                    new AdvertisedData(RPI140, AEM140),
+                    new AdvertisedData(RPI141, AEM141),
+                    new AdvertisedData(RPI142, AEM142),
+                    new AdvertisedData(RPI143, AEM143));
+
+    private TestVectors() {
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cca3d94
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.samples.ensnippets">
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name=".MainActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
\ No newline at end of file
diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..3c7076f
--- /dev/null
+++ b/app/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,60 @@
+#
+# Copyright 2020 Google LLC
+#
+# 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.
+#
+
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html
+
+# Sets the minimum version of CMake required to build the native library.
+
+cmake_minimum_required(VERSION 3.4.1)
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+
+add_library( # Sets the name of the library.
+             native-lib
+
+             # Sets the library as a shared library.
+             SHARED
+
+             # Provides a relative path to your source file(s).
+             native-lib.cpp )
+
+# Searches for a specified prebuilt library and stores the path as a
+# variable. Because CMake includes system libraries in the search path by
+# default, you only need to specify the name of the public NDK library
+# you want to add. CMake verifies that the library exists before
+# completing its build.
+
+find_library( # Sets the name of the path variable.
+              log-lib
+
+              # Specifies the name of the NDK library that
+              # you want CMake to locate.
+              log )
+
+# Specifies libraries CMake should link to your target library. You
+# can link multiple libraries, such as libraries you define in this
+# build script, prebuilt third-party libraries, or system libraries.
+
+target_link_libraries( # Specifies the target library.
+                       native-lib
+
+                       # Links the target library to the log library
+                       # included in the NDK.
+                       ${log-lib} )
\ No newline at end of file
diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp
new file mode 100644
index 0000000..8760a80
--- /dev/null
+++ b/app/src/main/cpp/native-lib.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#include <jni.h>
+#include <string>
+
+extern "C" JNIEXPORT jstring JNICALL
+Java_com_google_samples_ensnippets_MainActivity_stringFromJNI(
+        JNIEnv* env,
+        jobject /* this */) {
+    std::string hello = "Hello from C++";
+    return env->NewStringUTF(hello.c_str());
+}
diff --git a/app/src/main/java/com/google/samples/ensnippets/MainActivity.kt b/app/src/main/java/com/google/samples/ensnippets/MainActivity.kt
new file mode 100644
index 0000000..867e434
--- /dev/null
+++ b/app/src/main/java/com/google/samples/ensnippets/MainActivity.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.ensnippets
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import kotlinx.android.synthetic.main.activity_main.*
+
+/**
+ * This is NOT a usable application and was included only as convenience during development
+ * to ensure the code would compile.
+ *
+ * There are several tests included in the "androidTest" folder that can be run once the APK
+ * can be built.
+ */
+class MainActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+
+        // Example of a call to a native method
+        sample_text.text = stringFromJNI()
+    }
+
+    /**
+     * A native method that is implemented by the 'native-lib' native library,
+     * which is packaged with this application.
+     */
+    external fun stringFromJNI(): String
+
+    companion object {
+        // Used to load the 'native-lib' library on application startup.
+        init {
+            System.loadLibrary("native-lib")
+        }
+    }
+}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..139650a
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,46 @@
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="85.84757"
+                android:endY="92.4963"
+                android:startX="42.9492"
+                android:startY="49.59793"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..381ec0d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..5a7a7c6
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+    <TextView
+        android:id="@+id/sample_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Hello World!"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..72dc8b4
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..72dc8b4
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/raw/export.bin b/app/src/main/res/raw/export.bin
new file mode 100644
index 0000000..2af74e1
Binary files /dev/null and b/app/src/main/res/raw/export.bin differ
diff --git a/app/src/main/res/raw/sample_diagnosis_key_file.zip b/app/src/main/res/raw/sample_diagnosis_key_file.zip
new file mode 100644
index 0000000..2560831
Binary files /dev/null and b/app/src/main/res/raw/sample_diagnosis_key_file.zip differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..50c6a61
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<resources>
+    <color name="colorPrimary">#6200EE</color>
+    <color name="colorPrimaryDark">#3700B3</color>
+    <color name="colorAccent">#03DAC5</color>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..f0f2802
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<resources>
+    <string name="app_name">Exposure Notifications Snippets</string>
+</resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..2bcaede
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,26 @@
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..7ed1294
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+    ext.kotlin_version = "1.3.72"
+    repositories {
+        google()
+        jcenter()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.0.1'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.12"
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/en-risks-and-mitigations-faq.md b/en-risks-and-mitigations-faq.md
new file mode 100644
index 0000000..beab86d
--- /dev/null
+++ b/en-risks-and-mitigations-faq.md
@@ -0,0 +1,712 @@
+# Exposure Notification: Risks and Mitigations FAQ
+
+This document explains the key design decisions that affect the privacy and
+security of the Google/Apple Exposure Notifications system (ENS), the risks that
+were evaluated, and the mitigations that are in place for these risks. Details
+of the ENS design can be found
+[here](https://www.google.com/covid19/exposurenotifications/).
+
+# Key design decisions and constraints relevant to security and privacy
+
+## Decentralized architecture
+
+The design of an effective exposure notification system requires trade-offs
+between efficacy, privacy, and security. A common way of categorizing contact
+tracing and Exposure Notification designs is between:
+
+1.  **Centralized.** A central service makes decisions about whether to
+    notify someone if they have been exposed or not. Centralized designs
+    require devices to share a list of observed Bluetooth identifiers with a
+    central notification service.
+2.  **Decentralized.** The user's device decides locally whether to notify
+    someone if they have been exposed or not. Decentralized designs require
+    devices to receive a list of all COVID-positive users' broadcast Bluetooth
+    identifiers.
+
+The Google/Apple design uses a decentralized architecture because:
+
+1.  Devices of COVID-negative users do not share any information about
+    social interactions, location, or infection status. Decentralized designs
+    provide better privacy protection for COVID-negative participants compared
+    to centralized designs, since they do not require sharing any information
+    with a central service unless a user declares themselves COVID-positive.
+    This is especially important since we assume that the vast majority of
+    participants will be COVID-negative.
+    In contrast, centralized designs have the property that any party with
+    access to the central service has access to observed Bluetooth Low Energy
+    (BLE) identifiers—and therefore information about social interactions—for
+    all participants, including those who have not declared themselves as
+    COVID-positive.
+2.  For COVID-positive users, the information shared with the server in a
+    decentralized model is their COVID-positive status rather than any
+    information about social interactions: decentralized designs require
+    sharing broadcast Rolling Proximity Identifiers (RPIs) of COVID-positive
+    users with all participating devices (usually via a central service).
+    Centralized designs expose users' social interactions to a central service
+    in order to receive notifications. Even if care is taken not to store
+    information on relationships, the centralized designs inherently risk that
+    this information may be obtained by an adversary.
+    COVID-positive status data presents less incremental risk to the user and
+    is less permanent than a person's social interactions: see
+    [here](#covid-positive) for more details.
+3.  Centralized designs also expose COVID-positive status. While centralized
+    designs can minimize information revealed to the public at large, and make
+    such attacks more difficult, they cannot prevent attacks that identify
+    COVID-positive users entirely.
+    In a centralized model an adversary can emit Bluetooth IDs in the vicinity
+    of a device whose owner has been identified via other means, for example,
+    by using vehicle number plate recognition. Once this device reports its
+    owner as COVID-positive, the adversary's device will receive an exposure
+    notification, which can be tied to the identified user. If they want to do
+    this multiple times, however, they would need to have a single Healthcare
+    Authority account, that is notification channel, per identified user.
+    Healthcare Authority diagnosis servers could implement defenses against
+    Sybil attacks using multiple accounts, but this could put legitimate users
+    at higher risk of being falsely excluded from receiving exposure notifications.
+
+## Location
+
+Location data from the device is not used by the Google/Apple EN API in any
+way.
+
+Healthcare Authority applications using the Exposure Notification API are also
+subject to the condition that applications do not make use of location APIs, and
+do not record or store location information from the device.
+
+EN respects the Android requirement for all Bluetooth scanning services to
+enable the device location setting. This requirement has been present in the
+Android platform since 2015, and is designed to protect the user's privacy by
+preventing apps from using nearby Bluetooth beacons to locate the user when the
+user has explicitly turned off all location for the device.
+
+Enabling this setting by itself does _not_ mean that an application is using the
+device's location: the location runtime permission is _also_ required for any
+application to access the device's location. Android users can still decide
+whether any app they've installed has permission to access the location of their
+device, even if the device location setting is on.
+
+## Bandwidth usage
+
+The decentralized model requires a larger amount of data to be downloaded by
+each device. In the EN, devices download a highly compressed version of 14 days
+worth of Rolling Proximity Identifiers of every COVID-positive user. Using daily
+exposure keys achieves approximately 100x compression by allowing the 15 minute
+RPIs to be regenerated from daily seeds. We estimate that 14 days' worth of
+daily keys for 100,000 COVID-positive people is 22.4 MB. We recommend that EN
+apps download keys when the device is connected to wifi and power if possible.
+
+## Use of connectionless protocol
+
+The ENS uses a connectionless protocol. In our experience, protocols that
+require BLE connections or other forms of two-way interactions between
+participants' phones result in error-prone implementations, and can have
+significant negative effects on battery life. This led to a decision to use
+connectionless BLE advertisements as part of the protocol. One consequence of
+this is that the size of proximity identifiers is constrained to 20 bytes.
+
+# Risks and Mitigations
+
+The following is an analysis of security and privacy risks to the system and its
+users that were considered and mitigated to the extent possible within the above
+design constraints.
+
+## Disruption
+
+**Concern**
+
+Adversaries may attempt to disrupt the EN to create fraudulent exposure
+notifications. Such fraudulent exposure notifications may result in
+individuals or targeted groups unnecessarily self-quarantining. Disruptions
+could also decrease the effectiveness of the system and increase its cost
+of operation to both users and networks by, for example, increasing
+bandwidth costs for users.
+
+### RPI Spoofing using reported diagnosis keys
+
+**Concern**
+
+An adversary may try to use diagnosis keys downloaded from the server to
+generate and broadcast proximity identifiers in an attempt to create false
+ENs to users receiving them.
+
+**Mitigations**
+
+The EN implementations in both Android and iOS manage temporary exposure
+keys on behalf of client apps. When a user tests positive, the user can
+instruct their client app to upload their keys.
+
+Both Android and iOS only return keys that are no longer in use. This means
+that only expired keys are uploaded and revealed publicly. The matching
+algorithm prevents a proximity identifier generated by expired keys from
+leading to Exposure Notifications, mitigating this replay attack scenario.
+
+### Relaying RPIs
+
+**Concern**
+
+An adversary could record proximity identifiers being broadcast by
+legitimate users and rebroadcast them elsewhere. This could be done for
+users likely to be diagnosed soon thereafter—for example, near a hospital
+or testing center—and then rebroadcast to target a large number of users.
+Alternatively, a large number of collected RPIs could be rebroadcast over a
+large group, increasing the notification rate for that group in line with
+the increased broadcast rate, effectively simulating being in a crowd.
+
+A second variant, where the adversary is able to compromise the test
+verification process, is to broadcast RPIs from Temporary Exposure Keys
+(TEKs) that are subsequently uploaded to a diagnosis key server.
+
+**Mitigations and considerations**
+
+We acknowledge that a sophisticated adversary could mount relay attacks
+against our system but we note that:
+
+*   Any potential attack would require relayed broadcasts to persist for
+    as long as the Healthcare Authority app threshold for an exposure, for
+    example, 5-10 minute duration of close proximity to the target devices.
+    This would require high powered antennas for most targets, which both
+    increases the complexity of an attack and makes it more detectable.
+*   Potential device-based malware that rebroadcasts BLE RPIs
+    would have to be deployed at large scale to be successful. Any such malware
+    detected during Google Play's rigorous review process would be removed.
+*   Deploying fixed BLE listeners and broadcasters requires non-trivial
+    physical setup and a source of power, which puts adversaries and their
+    equipment at risk of discovery.
+*   A hypothetical attack of this type could readily be detected by
+    monitoring the rate of exposure notifications for anomalous changes that
+    are inconsistent with an epidemiologically realistic model and/or data
+    from other sources, such as manual contract tracing.
+
+    For example, if mutually independent sources—e.g. test results and
+    manual contact tracing data—show the infection rate is stable at 0.1% in
+    a specific area, but exposure notifications increase tenfold, this would
+    be a clear abuse signal.
+*   A mechanism to verify that keys uploaded are tied to a positive test
+    result is strongly recommended to all Health Authorities to limit the
+    ability to upload fake diagnosis keys.
+    Google has published a reference verification service design
+    [here](https://developers.google.com/android/exposure-notifications/verification-system)
+    and open source code
+    [here](https://github.com/google/exposure-notifications-verification-server).
+    This design is built with two foundational privacy goals in mind:
+
+    *   The diagnosis key server should not learn the identity of a
+        COVID-positive individual
+    *   The verification server and the Healthcare Authority should not
+        learn the TEKs of the COVID-positive individual
+
+#### Additional considerations
+
+Relay attacks are well understood and have been studied for many years:
+all mitigations require either location information, which creates a new
+privacy risk for ENS, or precise timing information. Timing-based solutions
+are not feasible in this system since BLE packets can be relayed almost
+instantaneously over the internet. Matching in ENS must be tolerant to
+legitimate variations in timing such as processor speed. Distance-bounding
+protocols that also rely on precise timing were discounted because they
+require a 2-way protocol (see [connectionless
+protocols](#use-of-connectionless-protocol)) and the high degree of precision
+required is not supported by most BLE device firmware. That said, we
+continue to evaluate potential mitigations within the parameters of our
+design goals.
+
+**Concern**
+
+Associated Encrypted Metadata (AEM) TX (transmit) power could be altered as
+part of a relay attack. We do not believe that TX power authentication
+would be a useful defense against relay attacks for the following reasons:
+
+*   TX power authentication doesn't protect against the use of a
+    high-power antenna positioned to cover a large area with a signal strength
+    indicative of proximity. This is because the system is designed to be
+    highly tolerant of differences between TX-power and received signal
+    strength indicator (RSSI) caused, for example, by both devices being
+    in-pocket. The Android ecosystem also has a diverse range of hardware: some
+    Android devices transmit signals as much as 30dB weaker than others. The
+    required system tolerance to differences in transmit power means that
+    relaying a packet from such a weak device on a higher transmit power device
+    would already allow the higher power device to appear nearby without
+    altering the TX field.
+*   TX power authentication would not prevent the deployment of malware on
+    phones to perform relay packets; app-store and OS policy are more effective
+    to prevent collection of EN packets by third party apps.
+*   In this context, encrypting the AEM prevents an adversary from joining
+    between RPIs coming from the same device (using device-specific TX power
+    as a source of entropy).
+
+
+### Server data pollution
+
+**Concern**
+
+An adversary broadcasts RPIs based on adversary-chosen diagnosis keys to
+many targets, then fraudulently uploads the keys as if they belonged to a
+legitimately diagnosed individual. Even without broadcasting RPIs based on
+these keys, this could be used to disrupt the service since an excessive
+number of keys would result in unacceptable bandwidth and CPU usage to
+download and match keys on devices.
+
+**Mitigations**
+
+*   We strongly recommend that all Health Authorities implement a
+    mechanism to verify that keys uploaded are tied to a positive test result.
+    This will limit the ability to upload fake diagnosis keys. See [Relaying
+    RPIs](#relaying-rpis) for more details
+*   Servers are also strongly encouraged to implement standard denial of
+    service/abuse prevention mechanisms.
+
+## Learning COVID-positive status
+
+### Re-identification
+
+**Concern**
+
+During the 14 day exposure window, an adversary could collect a target's
+Rolling Proximity Identifiers and then correlate them with published
+diagnosis keys. The adversary (who could be a legitimate participant in EN)
+could then identify the source of the exposure based on information
+external to the system, for example, using knowledge of who was present at
+the same time and place.
+
+To succeed, the adversary must simultaneously identify the specific target
+(visually, for example via number plate recognition, at a border control
+point, physical access controls to buildings) and record the RPI
+transmitted by an EN app on the target's mobile device. If both can be done
+at the same time, the adversary can discover that the target was
+COVID-positive after they upload their Temporary Exposure Keys (TEKs).
+If multiple users of the EN app are within range, the adversary will have
+to distinguish between several users' RPIs, though signal strength could be
+used to narrow identification. Conclusive identification of the target's
+diagnosis status might require several periods of RPI capture during the
+same day. TEKs are randomly and independently generated using a
+cryptographic random number generator (see
+[EN Cryptography Specification](https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf)).
+Therefore, an adversary would have to carry out re-identification attacks
+separately for each day.
+
+**Mitigations**
+
+The core purpose of an EN system is to learn whether any COVID-positive
+people have been close enough for long enough to put the device owner at
+risk of infection. Without this ability, the system doesn't do what it was
+designed to. The alternative is not to operate an EN system at all. While
+ENS apps do not have access to scanned RPIs via the ENS APIs, it's not
+possible to prevent a sophisticated adversary from capturing BLE RPIs using
+a dedicated eavesdropping device.
+
+<a id="covid-positive">With</a> this in mind, an important motivation in deciding between centralized
+and decentralized has been that COVID-positive status data presents less
+incremental risk to a person, and is less permanent than a person's social
+interactions, which are exposed by a centralized approach. For example:
+
+*   COVID-positive status may only be accurate for a few days after
+    someone reports their diagnosis keys
+*   Many celebrities have publicly shared their COVID-positive status on
+    social media
+*   Governments keep records of COVID-positive individuals—separate from
+    diagnosis key servers—whether or not an ENS is in operation
+*   COVID-positive status is easily observed by other means; for example,
+    neighbors or the press can often observe if someone has been hospitalized
+*   Some already operational government contact tracing discloses a person's
+    COVID-positive status to their contacts
+
+#### Additional considerations
+
+**Split key** approaches are one proposed mitigation: multiple pieces
+(cryptographic secret shares) of an RPI, broadcast at different times, must
+be observed before a match with a reported diagnosis key is possible. To
+succeed, a re-identifying adversary would have to be near the target for
+long enough to successfully match. We have not implemented such a scheme
+for several reasons:
+
+*   The requirements for an adversary seem no different from the
+    Healthcare Authority-defined criteria for an exposure
+*   The risk of missing relevant contacts increases as the split key window
+    increases. Our design avoids situations where valid contacts are otherwise lost.
+*   The encounter duration threshold for a valid contact can be set by an EN
+    app's Healthcare Authority at matching time. A split key solution requires
+    this threshold to be decided at broadcast time, which reduces the
+    flexibility to modify duration thresholds, for example, in roaming scenarios.
+
+We note that centralized solutions are also vulnerable to a targeted
+re-identification attack. In the centralized case, the adversary broadcasts
+RPIs in proximity to their target, shares the RPIS with the central
+service, and then observes whether or not they receive an Exposure
+Notification.
+
+### Network traffic analysis
+
+**Concern**
+
+An adversary could learn about infection status by observing network
+traffic between devices and servers, including diagnosis servers.
+
+**Mitigations**
+
+*   Use Transport Layer Security (TLS) to protect the integrity and
+    confidentiality of network data.
+*   We recommend that Healthcare Authority apps make randomized requests to
+    servers to prevent an adversary from concluding a user was diagnosed based
+    on observing network traffic to the diagnosis server that only happens upon
+    diagnosis. This is implemented in the Google open-source sample app
+[here](https://github.com/google/exposure-notifications-android/blob/master/app/src/main/java/com/google/android/apps/exposurenotification/network/DiagnosisKeyUploader.java#L115).  
+
+### Diagnosis server compromise
+
+**Concern**
+
+An adversary with access to the data on the diagnosis server could use this
+information to potentially identify users.
+
+**Mitigations**
+
+Diagnosis keys do not identify users. They are randomly generated and not
+inherently linked to individuals' identities. Each person's diagnosis keys
+are generated independently each day and are unrelated.
+Google's API Terms of Service forbid associating diagnosis keys with
+personally identifiable information, or with other diagnosis keys from the
+same device, except temporarily for verification purposes to support
+certain already deployed verification flows. We also recommend that
+diagnosis server operators limit retention of potentially identifiable
+information including, but not limited to, server logs that contain IP
+addresses.
+
+### Compromised mobile device
+
+**Concern**
+
+EN apps store information on-device that would allow an adversary to
+determine the device user's diagnosis status. For example, whether or not
+the user is participating in EN and hasn't yet uploaded diagnosis keys.
+
+**Mitigations**
+
+Android includes multiple strong layers of defense against device
+compromise, including [Google Play
+Protect](https://www.android.com/play-protect/). If an adversary is able to
+compromise the device, much more sensitive information is available,
+including the user's contacts, messaging history, emails, and photos.
+
+### Forensics and physical access to devices
+
+**Concern**
+
+An adversary with physical access to the device could extract historical
+information such as observed RPIs or the TEKs used to generate broadcast
+RPIs
+
+**Mitigations**
+
+*   Whenever a device supports full-disk encryption (FDE) or file-based
+	encryption (FBE), all data stored on disk is encrypted using this
+	standard mechanism by default. For devices that do not support either of
+	these, encryption-at-rest of data has no benefit because the key would
+	need to be stored in plaintext alongside the data. Such encryption is
+	mandatory on all devices running Android 10 and above and on all devices
+	running Android 6.0 or later, excluding low-RAM devices and devices on
+	which AES bulk encryption performance is below 50MB/s. 
+
+*   Encryption of data sent to the server and stored by the HA app is
+	the responsibility of these apps. Google’s sample app uses HTTPS and the
+	default FBE implementation available. Disabling HTTPS requires the HA to
+	set an explicit network security configuration setting in the APK
+	manifest.
+
+*   TEKs generated by the device are stored for 14 days before being
+	deleted by a daily maintenance process. The same is true for RPIs
+	scanned by the device and matches detected. Note that Healthcare
+	Authorities are also responsible for handling TEKs when reporting a
+	positive diagnosis. In this case, storage and deletion are under the
+	control of the HA app.
+
+*   Even when an attacker has physical access to local storage, platform
+	encryption protects the data. If an attacker were to try and compromise
+	system integrity (rooting) this would require unlocking the bootloader,
+	which on all modern Android devices automatically wipes user data as a
+	security precaution.
+
+*   Android devices do not have swap partitions. Where the related
+	feature ``zram writeback`` is used, compressed RAM is written to the
+	``/data/per_boot`` directory, which is encrypted with a newly generated key
+	on each boot and stored only in RAM.
+
+*   Devices using a physical secure element (SE) and implementing
+	File-Based Encryption (FBE, introduced in Android 7.0 and mandatory on
+	new devices since Android 10) store the Key Derivation Function (KDF)
+	secret in the SE and destroy it upon Factory Data Reset (FDR), thus
+	rendering data irrecoverable;
+
+*   Devices using the SE in general provide non-recoverability guarantees
+	that are at least as strong as the physical and logical integrity of the
+	SE.
+
+*   Even if they do not have a secure element (SE), devices running
+	either FBE or full-disk encryption (FDE) generally require at least the
+	physical compromise of the flash memory to recover any data after FDR.
+
+## Learning social interactions
+
+**Concern**
+
+An adversary could potentially identify a participant of EN as the origin
+of an Exposure Notification. This would also reveal social interactions
+since it implies the two were in proximity.
+
+An adversary with access to a device's EN APIs could feed a custom match
+query to the provideDiagnosisKeys API with a known, identified diagnosis
+key. If the API returns a match, the adversary knows that the individual
+with the diagnosis key exposed the device owner, that they met, and for how
+long.
+
+An adversary could also feed a group of diagnosis keys to the API to learn
+if a user associated with the group of keys exposed the device owner. TEK
+metadata could be used to increase the effectiveness of this potential
+attack by observing matching results for a subset of diagnosis keys, tagged
+using TEK metadata. This could involve chaining between multiple
+observations if metadata state changes are possible.
+
+Location variant: If the RPIs associated with that group of diagnosis keys
+were broadcast from a fixed location known to the adversary, the same
+attack could be used to learn about a target's location.
+
+**Mitigations**
+
+This type of potential attack would require connecting a user's identity
+with any diagnosis keys used in the attack. Mitigations against this attack
+include:
+
+1. Logging hashes of queries and making them exportable in the UI with the
+user's consent. These keys can be pooled for analysis to detect targeted
+attacks.
+
+2. The diagnosis server must sign all key bundles used in matching. Invalid
+signatures will result in an error upon matching. This prevents an attack
+via a compromised app that submits malicious queries. This mechanism also
+lets servers control the queries made and rate-limit them.
+
+3. Starting with Exposure Notification API v1.5, Healthcare Authority apps
+can only make six queries for `ExposureWindow` per day, reducing the
+risk of brute force attacks of this type.
+
+4. Metadata state changes are limited to only one per key. Such state
+changes are used, for example, to allow self-reported keys to transition to
+a confirmed diagnosis.
+
+5. The API can only be called from a limited set of known, approved apps.
+This means that an adversary must either have root-exploited the device or
+has compromised an Exposure Notification app's code signing keys.
+
+6. Connecting diagnosis keys with a user's identity for any purpose other
+than test verification is against
+[Google's additional terms of service for ENS apps](https://blog.google/documents/72/Exposure_Notifications_Service_Additional_Terms.pdf).
+
+#### Additional considerations
+
+Requiring a minimum query size would not help mitigate this attack
+because the API has no way to differentiate between a valid and invalid
+diagnosis key. An adversary could fill the rest of the query with fake or
+COVID-negative keys.
+
+## Tracking
+
+### Tracking COVID-positive users for the rolling window period using the Temporary Exposure Key
+
+**Concern**
+
+An adversary with access to the downloaded TEKs could link a
+COVID-positive user's RPIs for the duration of a rolling window period.
+Using BLE sniffers in multiple places, an adversary could link together
+different observations of the user over the rolling window period but not
+longer.
+
+See also [Learning Social Interactions: location
+variant](#learning-social-interactions) above.
+
+**Mitigations and considerations**
+
+Linking Rolling Proximity Identifiers with a Temporary Exposure Key for the
+duration of the Rolling Window is a network compression mechanism that
+reduces bandwidth costs for all users by approximately 100X. (See
+[Bandwidth Usage](#bandwidth-usage) above). The impact of this
+effect is greater in high-population countries where bandwidth is
+relatively costly. Our design must balance cost and network load with
+limiting trackability. We note that:
+
+1.  This attack would allow trackability **only** **for declared
+    COVID-positive users** and only over the rolling period window, which is 24
+    hours in EN version 1.5. Users are clearly informed and can opt-out when
+    uploading TEKs following a positive diagnosis.
+2.  For users who have not consented to upload their diagnosis keys
+    following a positive COVID test result, an adversary cannot learn any more
+    about the user than they already can by sniffing BLE packets.
+
+    Rolling Proximity Identifiers are designed to be no more linkable than
+    standard Bluetooth broadcast packets, which include a frequently rotating
+    MAC address. Bluetooth Low-Energy (BLE) MAC addresses rotate at least every
+    15 minutes, at the same time as the EN RPI. Therefore, a COVID-negative
+    person's device can at most be recognized for 15 minutes by another device
+    within physical BLE range. RPI intervals are synchronized with these
+    rotating Bluetooth MAC addresses to prevent linking alternately based on
+    MAC address and RPI.
+
+    By restarting the BLE interface on RPI reset, EN creates a random rotation
+    interval for all devices that is synchronized with BLE MAC rotation. This
+    prevents the BLE MAC being used to bridge the RPI rotation.
+3.  This attack would require a large network of BLE sniffers.
+4.  Unless the adversary has an independent means (for example, a camera
+    network) of identifying users whose devices are transmitting BLE packets,
+    the adversary will not learn the identity of COVID-positive users. If an
+    adversary has a mechanism for identifying users in proximity, then they
+    could already use this to track the user's movements. Using EN does not
+    significantly increase tracking risks.
+5.  To reduce the impact on the user's network bandwidth, the rolling period
+    window is set at 24 hours but can be reduced in lower infection rate
+    scenarios to lessen risks.
+6.  The movements and interactions of COVID-positive users are revealed to a
+    much greater extent by some manual contact tracing methods.
+7.  Google Play’s policies forbid the malicious use of BLE scanning, and
+    Play’s rigorous review processes are designed to detect it. Any app
+    found to be explicitly capturing BLE RPIs will be removed.
+	
+#### Additional considerations
+
+**Cuckoo filter approach as a mitigation**
+
+One mitigation that has been proposed
+[[see DP3T White Paper](https://github.com/DP-3T/documents/blob/master/DP3T%20White%20Paper.pdf)]
+is to distribute RPIs using a cuckoo filter rather than using temporary
+exposure keys and a pseudo-random function as in the current EN design.
+
+A cuckoo filter approach trades a reduction in bandwidth usage for a
+higher number of false-positives and reduced trackability. We decided
+against this approach because achieving an acceptable number of
+false-positives would require excessive bandwidth and device storage
+consumption.
+
+An in-depth analysis of the tolerable false-positive rate as part of our
+initial design led us to use 128-bit RPIs. Reaching a similarly low
+false-positive rate would make the use of cuckoo filters unfeasible from a
+bandwidth and storage perspective, given that the cuckoo filter is used for
+matching with RPIs, 144x more of them than Temporary Exposure Keys. For
+example, even allowing for a significantly higher false-positive rate for
+large deployments, for a daily infection rate of 60,000 with 50 million
+users, the download sizes required by each user would be over 1 GB per day.
+
+Unlike our current design, another concern with the use of cuckoo filters
+is that a match can be caused without the knowledge of the TEK that
+generated that RPI value. This means that clients performing RPI matching
+cannot verify that a corresponding TEK from which they were derived was
+released, or when they were supposed to be broadcast. This makes the time
+window for messages to be replayed longer.
+
+Using a cuckoo filter as a first stage match with a full check following
+would leak information about the user's social interactions to the server,
+which goes against a core privacy property of the system.
+
+### Bluetooth-based tracking
+
+**Concern**
+
+Switching on Bluetooth for users who had it switched off enables any
+tracking risk present with the standard Bluetooth stack.
+
+**Mitigations and considerations**
+
+The incremental risk of turning Bluetooth on to enable EN should be
+considered small because:
+
+*   All BLE packet MAC addresses rotate at least every 15 minutes in sync
+    with the EN RPI. At most, a person's device can be recognized for 15
+    minutes by another device within physical BLE range.
+*   7-15 minute rotating BLE advertising IDs are used by Android devices
+    starting with Android Marshmallow, that is approximately 80% of devices in
+    use. A large percentage of pre-Marshmallow devices do not have BLE, and
+    would therefore not be able to run EN. Bluetooth Classic uses a fixed MAC
+    address but this is only advertised if the device is in discoverable mode,
+    that is when Bluetooth settings are open or when an app has triggered this
+    mode, with a short timeout. Some manufacturers' Android devices enable
+    Bluetooth Classic in discoverable mode whenever Bluetooth is turned on. We
+    have notified, and are working with, affected manufacturers to fix this
+    vulnerability. In the meantime, on Android, we turn off discoverability the
+    first time EN is run, and every 24 hours thereafter.
+*   Unless the adversary has an independent means of identifying users whose
+    devices are transmitting BLE packets ( for example, a network of cameras)
+    an adversary could not learn the identity of users or anything about who
+    associates with whom. If an adversary has a mechanism for identifying users
+    in proximity, then they can already use this to track the users' movements,
+    and BLE does not introduce additional risk.
+
+**Bridging rotations at MAC/RPI boundaries**
+
+MAC and RPI rotations are designed to protect against adversaries
+observing the device’s BLE emissions across different locations and
+times, rather than continuously. For example, this mitigation protects
+against an adversary putting BLE sniffers in multiple train stations
+across a city, or against a store owner who links a user’s prior
+purchases to their path around the store.
+
+Conversely, protecting against an adversary who can continuously collect
+every BLE frame emitted by a device is not a design goal for either MAC
+or RPI rotation.
+
+*   This attack requires continuous proximity; an adversary would need
+    to have an independent means of tracking the user’s location, or a
+    spatially continuous network of sensors. For example, in order to track
+    a device leaving from home at 0900, going to a coffee shop at 0930, and
+    then to the library at 1100, the attacker must observe all 0915, 0930,
+    0945, 1000, … 1100 rotation events. If they could only observe the 0900
+    and 1100 rotations they would not be able to deduce that those were the
+    same device. 
+
+*   While this means that such attacks aren’t useful for an adversary,
+    it’s worth noting that the ability to bridge between temporally adjacent
+    broadcast IDs is inherent in the use of any RF protocol since: 
+
+    *   Adjacent timestamps can be used to link sources across frames
+
+    *   Signal strength can be assumed to be invariant between adjacent frames
+        and can therefore also be used to link sources across frames.
+
+For the sake of transparency, we note that this issue was confirmed on a
+subset of Android devices globally.  These issues likely resulted from
+how certain OEMs have implemented Bluetooth since, for the reasons noted
+above, the [Android Compatibility Definition Document](https://source.android.com/compatibility/10/android-10-cdd#7_4_3_bluetooth) (CDD) does not
+require rotation in sync.  After extensive testing, a change to EN has
+nevertheless been rolled out that removes this opportunity for
+device-specific misbehavior with respect to EN for all devices. The RPI
+is now set to a globally fixed value for a small number of BLE frames
+surrounding the RPI rollover.
+
+As noted above, Google Play’s policies forbid the malicious use of BLE
+scanning, and Play’s rigorous review processes are designed to detect
+it. Any app found to be explicitly capturing BLE RPIs will be removed.
+
+### Linking diagnosis keys through export file analysis
+
+**Concern**
+
+An adversary could attempt to link Diagnosis Keys from different days—which
+are independently randomly generated and _a priori_ not linkable—based on
+analysis of Diagnosis Key batches served by the Diagnosis server.
+
+**Mitigations**
+
+*   This may be feasible specifically in situations where very few
+    individuals are diagnosed positive in a given timeframe. If a diagnosis key
+    batch only contains one diagnosis key for each day, the keys in the batch
+    must correspond to the same individual. To mitigate this potential for
+    correlation, we recommend that diagnosis key servers pad out diagnosis key
+    batches with random keys, with some jitter so exports don't leak the fact
+    that they were padded. This is implemented in our reference server
+    ([code](https://github.com/google/exposure-notifications-server/blob/821531b167d794ec4a57075cd2009adbcd137505/internal/export/worker.go#L358)).
+*   The natural order in which diagnosis keys are stored in the diagnosis
+    server's database may confer some information about their association at
+    time of upload. To ensure this information is not leaked in diagnosis key
+    export batches, we recommend that batches are re-ordered before
+    publication, as we've done in our
+    [reference implementation](https://github.com/google/exposure-notifications-server/blob/1aef0fbe22aef93ec86f5432dd9d0317418eba15/internal/export/exportfile.go#L139).
+
+### Feedback
+
+If you have questions or feedback about this document, please let us know by
+emailing [ens-privsec@google.com](mailto:ens-privsec@google.com).
diff --git a/exposurenotification/.gitignore b/exposurenotification/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/exposurenotification/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/exposurenotification/build.gradle b/exposurenotification/build.gradle
new file mode 100644
index 0000000..8471423
--- /dev/null
+++ b/exposurenotification/build.gradle
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 org.apache.tools.ant.taskdefs.condition.Os
+apply plugin: 'com.android.library'
+apply plugin: 'com.google.protobuf'
+
+/*
+ * This library has dependencies upon a few other pieces of code, most notably NanoPB and
+ * BoringSSL. In order to compile this project, you must run the following bash scripts:
+ * $ bash third_party/nanopb/download.sh
+ * $ bash third_party/boringssl/build-android.sh
+ */
+def nanoPbRoot
+if (Os.isFamily(Os.FAMILY_MAC)) {
+    nanoPbRoot = file("${rootDir}/third_party/nanopb/macosx").absolutePath
+} else if (Os.isFamily(Os.FAMILY_UNIX)) {
+    nanoPbRoot = file("${rootDir}/third_party/nanopb/linux").absolutePath
+} else if (Os.isFamily(Os.FAMILY_WINDOWS)) {
+    nanoPbRoot = file("${rootDir}/third_party/nanopb/windows").absolutePath
+}
+def boringSslRoot = file("${rootDir}/third_party/boringssl/").absolutePath
+
+android {
+    compileSdkVersion 29
+    buildToolsVersion "30.0.0"
+    ndkVersion "21.3.6528147"
+
+    compileOptions {
+        // Flag to enable support for the new language APIs
+        coreLibraryDesugaringEnabled true
+        // Sets Java compatibility to Java 8
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 29
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+
+        /*
+         * - BORING_SSL_ROOT: location of the BoringSSL project root from
+         *     https://boringssl.googlesource.com/boringssl/
+         * - NANOPB_ROOT: location of NanoPB downloaded from
+         *     https://jpa.kapsi.fi/nanopb/download/
+         */
+        externalNativeBuild {
+            cmake {
+                cppFlags "-std=c++14"
+                arguments "-DBORING_SSL_ROOT=${boringSslRoot}", "-DNANOPB_ROOT=${nanoPbRoot}"
+            }
+        }
+        ndk {
+            // Explicitly set the supported ABI flavours
+            abiFilters "armeabi-v7a", "arm64-v8a", "x86"
+        }
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+    externalNativeBuild {
+        cmake {
+            path "src/main/cpp/CMakeLists.txt"
+            version "3.10.2"
+        }
+    }
+
+    testOptions {
+        unitTests {
+            includeAndroidResources = true
+        }
+    }
+}
+
+protobuf {
+    protoc {
+        artifact = 'com.google.protobuf:protoc:3.11.4'
+    }
+    dependencies {
+        protobuf files("${nanoPbRoot}/generator/proto/")
+    }
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                java {
+                    option "lite"
+                }
+            }
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar"])
+
+    // Java 8 backports
+    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
+
+    // AutoValue Dependencies
+    api "com.google.auto.value:auto-value-annotations:1.7.3"
+    annotationProcessor "com.google.auto.value:auto-value:1.7.3"
+
+    // Protobuf dependency
+    implementation 'com.google.protobuf:protobuf-javalite:3.11.4'
+
+    // Guava dependency
+    api "com.google.guava:guava:29.0-android"
+
+    // JODA
+    api 'joda-time:joda-time:2.9.4'
+
+    // Play Core library, so we don't have to reimplement the basics such as Task
+    implementation 'com.google.android.play:core:1.8.0'
+
+    api "androidx.core:core:1.3.1"
+    implementation "androidx.annotation:annotation:1.1.0"
+
+    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+    androidTestImplementation 'org.robolectric:robolectric:4.3.1'
+}
diff --git a/exposurenotification/consumer-rules.pro b/exposurenotification/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/exposurenotification/proguard-rules.pro b/exposurenotification/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/exposurenotification/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/exposurenotification/src/main/AndroidManifest.xml b/exposurenotification/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..50e0ccc
--- /dev/null
+++ b/exposurenotification/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright 2020 Google LLC
+  ~
+  ~ 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.
+  -->
+
+<manifest package="com.google.samples.matching">
+
+</manifest>
\ No newline at end of file
diff --git a/exposurenotification/src/main/cpp/CMakeLists.txt b/exposurenotification/src/main/cpp/CMakeLists.txt
new file mode 100644
index 0000000..7356fc7
--- /dev/null
+++ b/exposurenotification/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,90 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+#
+
+# For more information about using CMake with Android Studio, read the
+# documentation: https://d.android.com/studio/projects/add-native-code.html
+
+# Sets the minimum version of CMake required to build the native library.
+
+cmake_minimum_required(VERSION 3.4.1)
+
+# The Exposure Notification API depends on BoringSSL for cryptographic operations.
+add_library(crypto STATIC IMPORTED)
+add_library(ssl STATIC IMPORTED)
+
+# Configure the location of BoringSSL's libraries
+set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${BORING_SSL_ROOT}/${ANDROID_ABI}/crypto/libcrypto.a)
+set_target_properties(ssl PROPERTIES IMPORTED_LOCATION ${BORING_SSL_ROOT}/${ANDROID_ABI}/ssl/libssl.a)
+include_directories(matching ${BORING_SSL_ROOT}/src/include)
+
+# Determine the correct binary by platform
+if(WIN32)
+    set(PROTOC "protoc.exe")
+else()
+    set(PROTOC "protoc")
+endif()
+
+# Generate the NanoPB C source and headers
+file(MAKE_DIRECTORY ${PROJECT_SOURCE_DIR}/gen)
+execute_process(
+        COMMAND ${NANOPB_ROOT}/generator-bin/${PROTOC} -I ${NANOPB_ROOT} -I ${NANOPB_ROOT}/generator/proto/ -I ../proto --nanopb_out=./gen ../proto/exposure_key_export.proto
+        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+        OUTPUT_VARIABLE PROTOC_OUTPUT
+        RESULT_VARIABLE PROTOC_RESULT
+)
+# The generated code includes a header file, so add it to the include path
+include_directories(matching ${PROJECT_SOURCE_DIR}/gen)
+
+# NanoPB has some include files we'll need.
+include_directories(matching ${NANOPB_ROOT})
+
+# Creates and names a library, sets it as either STATIC
+# or SHARED, and provides the relative paths to its source code.
+# You can define multiple libraries, and CMake builds them for you.
+# Gradle automatically packages shared libraries with your APK.
+
+include_directories(matching ${CMAKE_LIBRARY_PATH}/protos)
+
+add_library(matching SHARED
+
+        # NanoPB code
+        # We just compile it ourselves since it's very small and simple
+        ${NANOPB_ROOT}/pb_encode.c
+        ${NANOPB_ROOT}/pb_decode.c
+        ${NANOPB_ROOT}/pb_common.c
+
+        # Protobuf Generated Code
+        gen/exposure_key_export.pb.c
+
+        # Key matching source
+        key_file_parser.cc
+        matching_helper.cc
+        matchingjni.cc
+        nanopb_encoder.cc
+        prefix_id_map.cc)
+
+# Searches for a specified prebuilt library and stores the path as a
+# variable. Because CMake includes system libraries in the search path by
+# default, you only need to specify the name of the public NDK library
+# you want to add. CMake verifies that the library exists before
+# completing its build.
+
+find_library(log-lib log)
+
+# Specifies libraries CMake should link to your target library. You
+# can link multiple libraries, such as libraries you define in this
+# build script, prebuilt third-party libraries, or system libraries.
+
+target_link_libraries(matching ${log-lib} crypto ssl)
diff --git a/exposurenotification/src/main/cpp/constants.h b/exposurenotification/src/main/cpp/constants.h
new file mode 100644
index 0000000..9d599fc
--- /dev/null
+++ b/exposurenotification/src/main/cpp/constants.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_CONSTANTS_H_
+#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_CONSTANTS_H_
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef __ANDROID__
+
+#include <android/log.h>
+
+#define LOG_TAG "ExposureNotificationJni"
+#define LOG_V(...) \
+  __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
+#define LOG_I(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+#define LOG_W(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
+#define LOG_E(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+#else
+#include <cstdio>
+#define LOG_V(...) fprintf(stderr, __VA_ARGS__)
+#define LOG_I(...) fprintf(stderr, __VA_ARGS__)
+#define LOG_W(...) fprintf(stderr, __VA_ARGS__)
+#define LOG_E(...) fprintf(stderr, __VA_ARGS__)
+#endif
+namespace exposure {
+    constexpr static const int kRpikLength = 16;
+    constexpr static const int kTekLength = 16;
+    constexpr static const int kIdLength = 16;
+    constexpr static const int kIdPerKey = 144;
+    constexpr static const int kHkdfInfoLength = 7;
+    constexpr static const char kHkdfInfo[] = u8"EN-RPIK";
+    constexpr static const int kRpiPaddedDataLength = 12;
+    constexpr static const char kRpiPaddedData[] = u8"EN-RPI\0\0\0\0\0\0";
+
+}  // namespace exposure
+
+#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_CONSTANTS_H_
diff --git a/exposurenotification/src/main/cpp/key_file_parser.cc b/exposurenotification/src/main/cpp/key_file_parser.cc
new file mode 100644
index 0000000..1ff568e
--- /dev/null
+++ b/exposurenotification/src/main/cpp/key_file_parser.cc
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#include "key_file_parser.h"
+
+namespace exposure {
+    bool VerifyHeader(pb_istream_t *pb_istream) {
+      char header[kFileHeaderSize] = {0};
+      pb_read(pb_istream, reinterpret_cast<pb_byte_t *>(header), kFileHeaderSize);
+      int result = memcmp(header, kFileHeader, kFileHeaderSize);
+      if (result != 0) {
+        LOG_E("Key file header mismatch %s, expected %s", header, kFileHeader);
+        return false;
+      }
+      return true;
+    }
+
+    std::unique_ptr<KeyFileIterator> CreateKeyFileIterator(
+        const std::string &key_file) {
+      FILE *file = fopen(key_file.c_str(), "rb");
+      if (file == nullptr) {
+        LOG_E("Failed to open file %s", key_file.c_str());
+        return nullptr;
+      }
+
+      auto buffer = std::make_unique<char[]>(kDefaultBufferSize);
+      setbuf(file, buffer.get());
+
+      auto pb_istream = CreatePbInputStream(file);
+      if (!VerifyHeader(&pb_istream)) {
+        LOG_E("Failed to verify the file header %s", key_file.c_str());
+        fclose(file);
+        return nullptr;
+      }
+
+      LOG_I("Created iterator for %s", key_file.c_str());
+      return std::make_unique<KeyFileIterator>(file, std::move(buffer), pb_istream);
+    }
+
+    void KeyFileIterator::ReadUntilNextKeyTagOrEnd() {
+      pb_wire_type_t wire_type;
+      bool eof = false;
+      while (!eof) {
+        pb_decode_tag(&pb_istream_, &wire_type, &next_tag_, &eof);
+        if (IsTagForKeys(next_tag_)) {
+          break;
+        }
+        pb_skip_field(&pb_istream_, wire_type);
+      }
+    }
+
+    std::unique_ptr<TemporaryExposureKeyNano> KeyFileIterator::ReadNextKey() {
+      if (!IsTagForKeys(next_tag_)) {
+        LOG_E("Unexpected proto buffer field");
+        return nullptr;
+      }
+
+      TemporaryExposureKeyNano key = TemporaryExposureKeyNano_init_default;
+      if (!pb_decode_delimited(&pb_istream_, TemporaryExposureKeyNano_fields,
+                               &key)) {
+        LOG_E("Failed to decode exposure key");
+        return nullptr;
+      }
+
+      ReadUntilNextKeyTagOrEnd();
+      return std::make_unique<TemporaryExposureKeyNano>(key);
+    }
+
+    bool ReadFromFileToStream(pb_istream_t *stream, pb_byte_t *buffer,
+                              size_t size) {
+      auto file = reinterpret_cast<FILE *>(stream->state);
+      size_t count = fread(buffer, 1, size, file);
+      if (ferror(file)) {
+        LOG_E("Failed to read input file stream");
+        return false;
+      }
+      if (feof(file)) {
+        stream->bytes_left = 0;
+      }
+      return count == size;
+    }
+
+    pb_istream_t CreatePbInputStream(FILE *file) {
+      pb_istream_t pb_istream;
+      pb_istream.callback = &ReadFromFileToStream;
+      pb_istream.state = reinterpret_cast<void *>(file);
+      pb_istream.bytes_left = std::numeric_limits<size_t>::max();
+      return pb_istream;
+    }
+}  // namespace exposure
diff --git a/exposurenotification/src/main/cpp/key_file_parser.h b/exposurenotification/src/main/cpp/key_file_parser.h
new file mode 100644
index 0000000..aae4366
--- /dev/null
+++ b/exposurenotification/src/main/cpp/key_file_parser.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_KEY_FILE_PARSER_H_
+#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_KEY_FILE_PARSER_H_
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "gen/exposure_key_export.pb.h"
+#include "constants.h"
+#include "pb_decode.h"
+
+#define TemporaryExposureKeyNano \
+  com_google_samples_exposurenotification_TemporaryExposureKey
+#define TemporaryExposureKeyExportNano_keys_tag \
+  com_google_samples_exposurenotification_TemporaryExposureKeyExport_keys_tag  // NOLINT
+#define TemporaryExposureKeyNano_init_default \
+  com_google_samples_exposurenotification_TemporaryExposureKey_init_default  // NOLINT
+#define TemporaryExposureKeyNano_fields \
+  com_google_samples_exposurenotification_TemporaryExposureKey_fields  // NOLINT
+
+namespace exposure {
+    static const char kFileHeader[] = "EK Export v1    ";
+    constexpr static const size_t kFileHeaderSize = sizeof(kFileHeader) - 1;
+    constexpr static const int kDefaultBufferSize = 64 * 1024;  // 64 KB
+
+    class KeyFileIterator {
+    public:
+        // The client of KeyFileIterator transfers the responsibility of closing
+        // `file` to KeyFileIterator, to avoid double closing.
+        explicit KeyFileIterator(FILE *file, std::unique_ptr<char[]> buffer,
+                                 pb_istream_t pb_istream)
+            : file_(file),
+              buffer_(std::move(buffer)),
+              pb_istream_(pb_istream),
+              next_tag_(0) {
+          ReadUntilNextKeyTagOrEnd();
+        }
+
+        ~KeyFileIterator() { fclose(file_); }
+
+        inline bool HasNext() const { return next_tag_ != 0; }
+
+        // Gets the next exposure key if HasNext() return true. If HasNext() return
+        // false or failed parse the proto message, return nullptr.
+        inline std::unique_ptr<TemporaryExposureKeyNano> Next() {
+          return ReadNextKey();
+        }
+
+    private:
+        void ReadUntilNextKeyTagOrEnd();
+
+        std::unique_ptr<TemporaryExposureKeyNano> ReadNextKey();
+
+        static inline bool IsTagForKeys(uint32_t tag) {
+          return tag == TemporaryExposureKeyExportNano_keys_tag;
+        }
+
+        FILE *file_;
+        std::unique_ptr<char[]> buffer_;
+        pb_istream_t pb_istream_;
+        uint32_t next_tag_;
+    };
+
+    std::vector<std::unique_ptr<TemporaryExposureKeyNano>> ParseFileDirectly(
+        const std::string &key_file);
+
+    std::vector<std::unique_ptr<TemporaryExposureKeyNano>> ParseKeysDirectly(
+        pb_istream_t *pb_istream);
+
+    bool VerifyHeader(pb_istream_t *pb_istream);
+
+    std::unique_ptr<KeyFileIterator> CreateKeyFileIterator(
+        const std::string &key_file);
+
+    pb_istream_t CreatePbInputStream(FILE *file);
+
+// NanoPB Callback, reads the specified size in bytes into buffer from the key
+// file 'stream->state', and set the internal state of stream accordingly.
+    bool ReadFromFileToStream(pb_istream_t *stream, pb_byte_t *buffer,
+                              std::size_t size);
+}  // namespace exposure
+
+#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_KEY_FILE_PARSER_H_
diff --git a/exposurenotification/src/main/cpp/matching_helper.cc b/exposurenotification/src/main/cpp/matching_helper.cc
new file mode 100644
index 0000000..0a21c51
--- /dev/null
+++ b/exposurenotification/src/main/cpp/matching_helper.cc
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#include "matching_helper.h"
+
+#include <openssl/aes.h>
+#include <openssl/cipher.h>
+#include <openssl/crypto.h>
+#include <openssl/digest.h>
+#include <openssl/hkdf.h>
+#include <stdio.h>
+
+#include <string>
+#include <vector>
+
+#include "key_file_parser.h"
+#include "nanopb_encoder.h"
+#include "prefix_id_map.h"
+
+constexpr char hexmap[] = {'0', '1', '2', '3', '4', '5', '6', '7',
+                           '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+std::string hexStr(const unsigned char *data, int len) {
+  std::string s(len * 2, ' ');
+  for (int i = 0; i < len; ++i) {
+    s[2 * i] = hexmap[(data[i] & 0xF0) >> 4];
+    s[2 * i + 1] = hexmap[data[i] & 0x0F];
+  }
+  return s;
+}
+
+extern "C" {
+
+namespace exposure {
+    MatchingHelper::MatchingHelper(JNIEnv *env, jobjectArray scan_record_ids) {
+      EVP_CIPHER_CTX_init(&context);
+
+      prefix_key_map = std::make_unique<PrefixIdMap>(env, scan_record_ids);
+      for (int i = 0; i < kIdPerKey; i++) {
+        memcpy(&aesInputStorage[i * kIdLength], kRpiPaddedData,
+               kRpiPaddedDataLength);
+      }
+      last_processed_key_count = 0;
+    }
+
+    MatchingHelper::~MatchingHelper() { EVP_CIPHER_CTX_cleanup(&context); }
+
+#if _BYTE_ORDER != _LITTLE_ENDIAN
+#error "Must use little endian"
+#endif
+
+    // PaddedData[0 - 5]: "EN-RPI".getBytes(UTF_8).
+    // PaddedData[6 - 11]: 0x000000000000.
+    // PaddedData[12 -15]: enIntervalNumber, uint32 little-endian.
+    bool MatchingHelper::GenerateIds(const uint8_t *diagnosis_key,
+                                     uint32_t rolling_start_number, uint8_t *ids) {
+      uint8_t rpi_key[kRpikLength];
+      // RPIK <- HKDF(tek, NULL, UTF8("EN-PRIK"), 16).
+      if (HKDF(rpi_key, kRpikLength, EVP_sha256(), diagnosis_key, kTekLength,
+          /*salt=*/nullptr, /*salt_len=*/0,
+               reinterpret_cast<const uint8_t *>(kHkdfInfo), kHkdfInfoLength) != 1) {
+        return false;
+      }
+
+      if (EVP_EncryptInit_ex(&context, EVP_aes_128_ecb(), /*impl=*/nullptr, rpi_key,
+          /*iv=*/nullptr) != 1) {
+        return false;
+      }
+
+      uint32_t en_interval_number = rolling_start_number;
+      for (int index = 0; index < kIdPerKey * kIdLength;
+           index += kIdLength, en_interval_number++) {
+        *((uint32_t *) (&aesInputStorage[index + 12])) = en_interval_number;
+      }
+
+      int out_length;
+      return EVP_EncryptUpdate(&context, ids, &out_length, aesInputStorage,
+                               kIdPerKey * kIdLength) == 1;
+    }
+
+    jobjectArray MatchingHelper::Matching(
+        JNIEnv *env, const std::vector<std::string> &key_files) {
+      std::vector<std::unique_ptr<TemporaryExposureKeyNano>> matched_keys;
+      uint8_t ids[kIdPerKey * kIdLength];
+      static_assert(AES_BLOCK_SIZE == kIdLength, "Incorrect kIdLength.");
+      last_processed_key_count = 0;
+      for (const auto &key_file : key_files) {
+        LOG_I("Matching with %s", key_file.c_str());
+        std::unique_ptr<KeyFileIterator> key_file_iterator(
+            CreateKeyFileIterator(key_file));
+        if (key_file_iterator.get() == nullptr) {
+          continue;
+        }
+        while (key_file_iterator->HasNext()) {
+          std::unique_ptr<TemporaryExposureKeyNano> key = key_file_iterator->Next();
+          if (key.get() == nullptr) {
+            continue;
+          }
+//          LOG_I("TEK: %s - %d, %d", hexStr(key->key_data.bytes, 16).c_str(),
+//                key->has_rolling_start_interval_number ? key->rolling_start_interval_number : -1,
+//                key->has_rolling_period ? key->rolling_period : -1);
+          last_processed_key_count++;
+          if (GenerateIds(key->key_data.bytes,
+                          static_cast<uint32_t>(key->rolling_start_interval_number),
+                          ids)) {
+            for (int j = 0; j < kIdPerKey * kIdLength; j += kIdLength) {
+              if (prefix_key_map->GetIdIndex(&ids[j]) >= 0) {
+                matched_keys.emplace_back(std::move(key));
+                break;
+              }
+            }
+          } else {
+            LOG_E("GenerateIds failed");
+          }
+        }
+      }
+
+      if (matched_keys.size() == 0) {
+        LOG_I("Matching done, total %d keys, no key matches",
+              last_processed_key_count);
+        return nullptr;
+      }
+
+      LOG_I("Matching done, total %d keys, find %d keys match",
+            last_processed_key_count, (int) matched_keys.size());
+
+      jobjectArray proto_array = env->NewObjectArray(
+          static_cast<jsize>(matched_keys.size()), env->FindClass("[B"), nullptr);
+      for (int i = 0; i < matched_keys.size(); i++) {
+        auto serialized = EncodeTemporaryExposureKey(matched_keys.at(i).get());
+        jbyteArray byte_array =
+            env->NewByteArray(static_cast<jsize>(serialized.size()));
+        env->SetByteArrayRegion(byte_array, 0,
+                                static_cast<jsize>(serialized.size()),
+                                reinterpret_cast<const jbyte *>(serialized.c_str()));
+        env->SetObjectArrayElement(proto_array, i, byte_array);
+        env->DeleteLocalRef(byte_array);
+      }
+      return proto_array;
+    }
+
+    // Converts a Java jbyteArray (encoding a UTF8 string) to a native UTF8 string.
+    std::string JbyteArrayToString(JNIEnv *env, jbyteArray input) {
+      jint len = env->GetArrayLength(input);
+      std::string output;
+      output.resize(len);
+      env->GetByteArrayRegion(input, 0, len,
+                              reinterpret_cast<jbyte *>(&(output)[0]));
+      return output;
+    }
+
+    jintArray MatchingHelper::MatchingLegacy(JNIEnv *env,
+                                             jobjectArray diagnosis_keys,
+                                             jintArray rolling_start_numbers,
+                                             int key_count) {
+      if (key_count > env->GetArrayLength(diagnosis_keys)) {
+        LOG_W("Key count not match diagnosis_keys numbers");
+        return nullptr;
+      }
+      if (key_count > env->GetArrayLength(rolling_start_numbers)) {
+        LOG_W("Key count not match rollingStartNumbers");
+        return nullptr;
+      }
+      LOG_I("Matching with %d diagnosis key", key_count);
+      std::vector<int> match_indexes;
+      jint *rolling_start_number_array =
+          env->GetIntArrayElements(rolling_start_numbers, 0);
+      uint8_t ids[kIdPerKey * kIdLength];
+      static_assert(kIdLength == AES_BLOCK_SIZE, "Incorrect kIdLength.");
+      uint8_t *key_bytes[kIdLength];
+      for (int i = 0; i < key_count; i++) {
+        jbyteArray key_array =
+            (jbyteArray) env->GetObjectArrayElement(diagnosis_keys, i);
+        env->GetByteArrayRegion(key_array, 0, kIdLength, (jbyte *) key_bytes);
+        if (GenerateIds(reinterpret_cast<const uint8_t *>(key_bytes),
+                        rolling_start_number_array[i], ids)) {
+          for (int j = 0; j < kIdPerKey * kIdLength; j += kIdLength) {
+            if (prefix_key_map->GetIdIndex(&ids[j]) >= 0) {
+              match_indexes.push_back(i);
+              break;
+            }
+          }
+        } else {
+          LOG_E("GenerateIds failed");
+        }
+        env->DeleteLocalRef(key_array);
+      }
+      env->ReleaseIntArrayElements(rolling_start_numbers,
+                                   rolling_start_number_array, 0);
+
+      if (match_indexes.size() == 0) {
+        LOG_I("Matching done, no key matches");
+        return nullptr;
+      }
+
+      LOG_I("Matching done, find %d keys match", (int) match_indexes.size());
+
+      jintArray result = env->NewIntArray((jsize) match_indexes.size());
+      env->SetIntArrayRegion(result, 0, match_indexes.size(), match_indexes.data());
+      return result;
+    }
+
+} // namespace exposure
+} // extern "C"
+
diff --git a/exposurenotification/src/main/cpp/matching_helper.h b/exposurenotification/src/main/cpp/matching_helper.h
new file mode 100644
index 0000000..5c6dbe0
--- /dev/null
+++ b/exposurenotification/src/main/cpp/matching_helper.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_MATCHING_HELPER_H_
+#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_MATCHING_HELPER_H_
+
+#include <jni.h>
+#include <openssl/aes.h>
+#include <openssl/cipher.h>
+#include <openssl/crypto.h>
+#include <openssl/digest.h>
+#include <openssl/hkdf.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "constants.h"
+#include "prefix_id_map.h"
+
+namespace exposure {
+    class MatchingHelper {
+    public:
+        MatchingHelper(JNIEnv *env, jobjectArray scan_record_ids);
+
+        ~MatchingHelper();
+
+        // Doing the matching, and return matched diagnosis_keys set.
+        jobjectArray Matching(JNIEnv *env, const std::vector<std::string> &key_files);
+
+        // Doing the matching, and return int[] for matched diagnosis_keys indexes.
+        jintArray MatchingLegacy(JNIEnv *env, jobjectArray diagnosis_keys,
+                                 jintArray rolling_start_numbers, int key_count);
+
+        bool GenerateIds(const uint8_t *diagnosis_key, uint32_t rolling_start_number,
+                         uint8_t *ids);
+
+        inline jint LastProcessedKeyCount() const { return last_processed_key_count; }
+
+    private:
+        std::unique_ptr<PrefixIdMap> prefix_key_map;
+        EVP_CIPHER_CTX context;
+        uint8_t aesInputStorage[kIdPerKey * kIdLength];
+        uint32_t last_processed_key_count;
+    };
+}  // namespace exposure
+#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_MATCHING_HELPER_H_
\ No newline at end of file
diff --git a/exposurenotification/src/main/cpp/matchingjni.cc b/exposurenotification/src/main/cpp/matchingjni.cc
new file mode 100644
index 0000000..91bb153
--- /dev/null
+++ b/exposurenotification/src/main/cpp/matchingjni.cc
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#include <jni.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <string>
+#include <vector>
+
+#include "constants.h"
+#include "matching_helper.h"
+#include "prefix_id_map.h"
+
+extern "C" {
+
+#define JND_PACKAGE(name) Java_com_google_samples_exposurenotification_matching_##name
+#define JND(name) JND_PACKAGE(MatchingJni_##name)
+
+JNIEXPORT jlong JNICALL JND(initNative)(JNIEnv *env, jclass clazz,
+                                        jobjectArray scan_id_records) {
+  if (scan_id_records == nullptr) {
+    LOG_W("Invalid input for initNative, scan records is null");
+    return 0;
+  }
+
+  if (env->GetArrayLength(scan_id_records) <= 0) {
+    LOG_W("Invalid input for initNative, scan records is empty");
+    return 0;
+  }
+
+  return reinterpret_cast<jlong>(
+      new exposure::MatchingHelper(env, scan_id_records));
+}
+
+JNIEXPORT jobjectArray JNICALL
+JND(matchingNative)(JNIEnv *env, jclass clazz, jlong native_ptr,
+                    jobjectArray key_files_jstring) {
+  if (native_ptr == 0 || key_files_jstring == nullptr) {
+    LOG_W("Invalid input for matchingNative");
+    return nullptr;
+  }
+
+  int key_file_count = env->GetArrayLength(key_files_jstring);
+  LOG_I("matchingNative get %d files", key_file_count);
+
+  std::vector<std::string> key_files;
+  for (int i = 0; i < key_file_count; i++) {
+    jstring key_file_jstring =
+        (jstring) (env->GetObjectArrayElement(key_files_jstring, i));
+    const char *key_file_string = env->GetStringUTFChars(key_file_jstring, 0);
+    key_files.push_back(std::string(key_file_string));
+    env->ReleaseStringUTFChars(key_file_jstring, key_file_string);
+    env->DeleteLocalRef(key_file_jstring);
+  }
+
+  exposure::MatchingHelper *wrapper =
+      reinterpret_cast<exposure::MatchingHelper *>(native_ptr);
+  return wrapper->Matching(env, key_files);
+}
+
+JNIEXPORT jintArray JNICALL JND(matchingLegacyNative)(
+    JNIEnv *env, jclass clazz, jlong native_ptr, jobjectArray diagnosis_keys,
+    jintArray interval_numbers, jint key_count) {
+  if (native_ptr == 0 || diagnosis_keys == nullptr ||
+      interval_numbers == nullptr) {
+    LOG_W("Invalid input for matchingNativeLegacy");
+    return nullptr;
+  }
+
+  LOG_I("matchingNative get %d keys", key_count);
+
+  exposure::MatchingHelper *wrapper =
+      reinterpret_cast<exposure::MatchingHelper *>(native_ptr);
+  return wrapper->MatchingLegacy(env, diagnosis_keys, interval_numbers,
+                                 key_count);
+}
+
+JNIEXPORT jint JNICALL JND(lastProcessedKeyCountNative)(JNIEnv *env,
+                                                        jclass clazz,
+                                                        jlong native_ptr) {
+  if (native_ptr == 0) {
+    LOG_W("Invalid input for lastProcessedKeyCount");
+    return -1;
+  }
+
+  exposure::MatchingHelper *wrapper =
+      reinterpret_cast<exposure::MatchingHelper *>(native_ptr);
+  return wrapper->LastProcessedKeyCount();
+}
+
+JNIEXPORT void JNICALL JND(releaseNative)(JNIEnv *env, jclass clazz,
+                                          jlong native_ptr) {
+  if (native_ptr == 0) {
+    LOG_W("Invalid input for releaseNative");
+    return;
+  }
+  exposure::MatchingHelper *wrapper =
+      reinterpret_cast<exposure::MatchingHelper *>(native_ptr);
+  delete wrapper;
+}
+} /* extern "C" */
\ No newline at end of file
diff --git a/exposurenotification/src/main/cpp/nanopb_encoder.cc b/exposurenotification/src/main/cpp/nanopb_encoder.cc
new file mode 100644
index 0000000..d697a86
--- /dev/null
+++ b/exposurenotification/src/main/cpp/nanopb_encoder.cc
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#include "nanopb_encoder.h"
+
+#include "constants.h"
+#include "pb_encode.h"
+
+namespace exposure {
+    std::string EncodeTemporaryExposureKey(
+        const TemporaryExposureKeyNano *temporary_exposure_key) {
+      uint8_t write_buffer[kEncodedBufferSize];
+      pb_ostream_t stream =
+          pb_ostream_from_buffer(write_buffer, kEncodedBufferSize);
+      if (!pb_encode(&stream, TemporaryExposureKeyNano_fields,
+                     temporary_exposure_key)) {
+        LOG_E("Failed to encode exposure key");
+        return nullptr;
+      }
+
+      return std::string(reinterpret_cast<char *>(write_buffer),
+                         stream.bytes_written);
+    }
+}  // namespace exposure
diff --git a/exposurenotification/src/main/cpp/nanopb_encoder.h b/exposurenotification/src/main/cpp/nanopb_encoder.h
new file mode 100644
index 0000000..4baa1e7
--- /dev/null
+++ b/exposurenotification/src/main/cpp/nanopb_encoder.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_NANOPB_ENCODER_H_
+#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_NANOPB_ENCODER_H_
+
+#include <string>
+
+#include "gen/exposure_key_export.pb.h"
+#include "key_file_parser.h"
+
+namespace exposure {
+    constexpr static const int kEncodedBufferSize = 64;
+
+    std::string EncodeTemporaryExposureKey(
+        const TemporaryExposureKeyNano *temporary_exposure_key);
+}  // namespace exposure
+#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_NANOPB_ENCODER_H_
diff --git a/exposurenotification/src/main/cpp/prefix_id_map.cc b/exposurenotification/src/main/cpp/prefix_id_map.cc
new file mode 100644
index 0000000..22067c5
--- /dev/null
+++ b/exposurenotification/src/main/cpp/prefix_id_map.cc
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#include "prefix_id_map.h"
+
+#include <jni.h>
+
+#include <algorithm>
+
+namespace exposure {
+
+    namespace {
+        uint16_t GetPrefixInner(const uint8_t *id) {
+          uint16_t ret;
+          memcpy(&ret, id, sizeof(ret));
+          return ret;
+        }
+
+        bool Compare(const std::string &lhs, const std::string &rhs) {
+          assert(lhs.size() >= 2);
+          assert(rhs.size() >= 2);
+          return GetPrefixInner(reinterpret_cast<const uint8_t *>(lhs.data())) <
+                 GetPrefixInner(reinterpret_cast<const uint8_t *>(rhs.data()));
+        }
+    }  // namespace
+
+    PrefixIdMap::PrefixIdMap(JNIEnv *env, jobjectArray ble_scan_records) {
+      memset(prefix_end_index, 0, sizeof(int) * kIdPrefixIndexSize);
+      scan_record_size = env->GetArrayLength(ble_scan_records);
+
+      for (int i = 0; i < scan_record_size; i++) {
+        jbyteArray single_id =
+            (jbyteArray) env->GetObjectArrayElement(ble_scan_records, i);
+        jbyte *id_bytes = env->GetByteArrayElements(single_id, 0);
+        scan_records.push_back(std::string((char *) id_bytes, env->GetArrayLength(single_id)));
+        env->ReleaseByteArrayElements(single_id, id_bytes, 0);
+        env->DeleteLocalRef(single_id);
+      }
+      std::sort(scan_records.begin(), scan_records.end(), Compare);
+      int last_prefix = 0;
+      for (int i = 0; i < scan_record_size; i++) {
+        int prefix =
+            GetPrefix(reinterpret_cast<const uint8_t *>(scan_records[i].data()));
+        while (last_prefix < prefix) {
+          prefix_end_index[last_prefix++] = i;
+        }
+      }
+      while (last_prefix < kIdPrefixIndexSize) {
+        prefix_end_index[last_prefix++] = scan_record_size;
+      }
+      LOG_I("PrefixIdMap load %d scan records", scan_record_size);
+    }
+
+    PrefixIdMap::~PrefixIdMap() { scan_records.clear(); }
+
+    int PrefixIdMap::GetIdIndex(const uint8_t *id) {
+      int prefix = GetPrefix(id);
+      int start_index = (prefix > 0) ? prefix_end_index[prefix - 1] : 0;
+      int end_index = prefix_end_index[prefix];
+      for (; start_index < end_index; start_index++) {
+        if (memcmp(id, scan_records[start_index].data(), exposure::kIdLength) ==
+            0) {
+          return start_index;
+        }
+      }
+      return -1;
+    }
+
+    uint16_t PrefixIdMap::GetPrefix(const uint8_t *id) {
+      return GetPrefixInner(id);
+    }
+}  // namespace exposure
diff --git a/exposurenotification/src/main/cpp/prefix_id_map.h b/exposurenotification/src/main/cpp/prefix_id_map.h
new file mode 100644
index 0000000..71dfeea
--- /dev/null
+++ b/exposurenotification/src/main/cpp/prefix_id_map.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_PREFIX_ID_MAP_H_
+#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_PREFIX_ID_MAP_H_
+
+#include <jni.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <string>
+#include <vector>
+
+#include "constants.h"
+
+namespace exposure {
+    constexpr static const int kIdPrefixIndexSize = 65536;
+
+    class PrefixIdMap {
+    public:
+        int prefix_end_index[exposure::kIdPrefixIndexSize];
+        std::vector<std::string> scan_records;
+        int scan_record_size;
+
+        // scanRecordIds is a byte[][], which contains all scanned ID from database.
+        // Due to we'll only try to copy its value to scan_records, so it's not
+        // owned by PrefixIdMap after construction.
+        PrefixIdMap(JNIEnv *env, jobjectArray ble_scan_records);
+
+        ~PrefixIdMap();
+
+        int GetIdIndex(const uint8_t *id);
+
+        uint16_t GetPrefix(const uint8_t *id);
+    };
+}  // namespace exposure
+#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_PREFIX_ID_MAP_H_
diff --git a/exposurenotification/src/main/cpp/testdata/export.bin b/exposurenotification/src/main/cpp/testdata/export.bin
new file mode 100644
index 0000000..f9a14f3
Binary files /dev/null and b/exposurenotification/src/main/cpp/testdata/export.bin differ
diff --git a/exposurenotification/src/main/java/com/google/samples/ByteArrayValue.java b/exposurenotification/src/main/java/com/google/samples/ByteArrayValue.java
new file mode 100644
index 0000000..a6a29a1
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/ByteArrayValue.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples;
+
+import androidx.annotation.Nullable;
+
+import com.google.auto.value.AutoValue;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Wrapper class providing value-semantics for {@code byte[]} fields, for example in @{@link
+ * AutoValue} classes.
+ *
+ * <p>The behaviours which give value semantics are:
+ *
+ * <ul>
+ *   <li>The constructor makes a copy of the input(s), to prevent mutation by the caller.
+ *   <li>The getter returns a copy of the array, to prevent mutation by the receiver.
+ *   <li>{@link #equals} is overridden to compare array content
+ *   <li>{@link #hashCode} is overridden to return the hashCode of the array content.
+ * </ul>
+ * <p>
+ * In some cases, the {@code clone()} is unnecessary overhead. Where it really matters, there is a
+ * constructor which can take ownership of the {@code byte[]}.
+ */
+public class ByteArrayValue {
+    private final byte[] bytes;
+
+    /**
+     * The length of the array.
+     */
+    public final int length;
+
+    public ByteArrayValue(byte[] bytes) {
+        this(bytes, /*takeOwnership=*/ false);
+    }
+
+    /**
+     * Constructor used for cases where the ByteArrayValue takes ownership of the {@code byte[]},
+     * avoiding the need to clone.
+     */
+    public ByteArrayValue(byte[] bytes, boolean takeOwnership) {
+        this.bytes = takeOwnership ? bytes : bytes.clone();
+        this.length = this.bytes.length;
+    }
+
+    public byte[] get() {
+        return bytes.clone();
+    }
+
+    /**
+     * Direct access to the internal {@code byte[]}. Use in derived classes for read-only access, to
+     * avoid the overhead of cloning the array.
+     *
+     * <p>WARNING: This breaks the encapsulation and could allow the value to be mutated. Only use in
+     * performance sensitive cases where the cost of cloning is too high, and the use of the {@code
+     * byte[]} is guaranteed to be read-only.
+     */
+    protected byte[] exposeInternalBytesAndRiskMutation() {
+        return bytes;
+    }
+
+    /**
+     * Compares the content of the array.
+     */
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof ByteArrayValue)) {
+            return false;
+        }
+        ByteArrayValue that = (ByteArrayValue) other;
+        return Arrays.equals(bytes, that.bytes);
+    }
+
+    /**
+     * Returns a hashCode for the content of the array.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(bytes);
+    }
+
+    @Override
+    public String toString() {
+        return new BigInteger(1, bytes).toString(16);
+    }
+
+    /**
+     * Construct a {@link ByteArrayValue} from a possibly null array.
+     *
+     * <p>If {@code bytes} is null, return null.
+     */
+    @Nullable
+    public static ByteArrayValue from(@Nullable byte[] bytes) {
+        return (bytes != null) ? new ByteArrayValue(bytes) : null;
+    }
+
+    /**
+     * Construct a value from the concatenation of 1 or more {@code byte[]} inputs.
+     */
+    public static ByteArrayValue concat(byte[]... arrays) {
+        byte[] bytes = concatBytes(arrays);
+        return new ByteArrayValue(bytes, /*takeOwnership=*/ true);
+    }
+
+    /**
+     * Puts the data from this {@link ByteArrayValue} into a {@link ByteBuffer}.
+     *
+     * @param byteBuffer the destination buffer to {@link ByteBuffer#put} the byte representation.
+     */
+    public void putIn(ByteBuffer byteBuffer) {
+        checkArgument(byteBuffer.remaining() >= length);
+        byteBuffer.put(bytes);
+    }
+
+    /**
+     * Returns the values from each provided array combined into a single array. For example, {@code
+     * concat(new byte[] {a, b}, new byte[] {}, new byte[] {c}} returns the array {@code {a, b, c}}.
+     *
+     * @param arrays zero or more {@code byte} arrays
+     * @return a single array containing all the values from the source arrays, in order
+     */
+    private static byte[] concatBytes(byte[]... arrays) {
+        int length = 0;
+        for (byte[] array : arrays) {
+            length += array.length;
+        }
+        byte[] result = new byte[length];
+        int pos = 0;
+        for (byte[] array : arrays) {
+            System.arraycopy(array, 0, result, pos, array.length);
+            pos += array.length;
+        }
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/Clock.java b/exposurenotification/src/main/java/com/google/samples/Clock.java
new file mode 100644
index 0000000..8eec6e4
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/Clock.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples;
+
+import android.os.SystemClock;
+
+/**
+ * Wrapper interface for time operations. Allows replacement of clock operations for testing.
+ */
+public interface Clock {
+
+    /**
+     * Get the current time of the clock in milliseconds.
+     *
+     * @return Current time in milliseconds.
+     */
+    long currentTimeMillis();
+
+    /**
+     * Returns milliseconds since boot, including time spent in sleep.
+     *
+     * @return Current time since boot in milliseconds.
+     */
+    long elapsedRealtime();
+
+    /**
+     * Returns the current timestamp of the most precise timer available on the local system, in
+     * nanoseconds.
+     *
+     * @return Current time in nanoseconds.
+     */
+    long nanoTime();
+
+    /**
+     * Returns the time spent in the current thread, in milliseconds
+     *
+     * @return Thread time in milliseconds.
+     */
+    @SuppressWarnings("StaticOrDefaultInterfaceMethod")
+    default long currentThreadTimeMillis() {
+        return SystemClock.currentThreadTimeMillis();
+    }
+
+    public static class DefaultClock implements Clock {
+
+        private static final DefaultClock instance = new DefaultClock();
+
+        public static Clock getInstance() {
+            return instance;
+        }
+
+        @Override
+        public long currentTimeMillis() {
+            return System.currentTimeMillis();
+        }
+
+        @Override
+        public long elapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+
+        @Override
+        public long nanoTime() {
+            return System.nanoTime();
+        }
+
+        @Override
+        public long currentThreadTimeMillis() {
+            return SystemClock.currentThreadTimeMillis();
+        }
+
+        private DefaultClock() {
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/Hex.java b/exposurenotification/src/main/java/com/google/samples/Hex.java
new file mode 100644
index 0000000..6add6f3
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/Hex.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples;
+
+/**
+ * Utilities related to hexadecimal byte representation.
+ */
+public class Hex {
+
+    private static final char[] HEX_UPPERCASE = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+    };
+    private static final char[] HEX_LOWERCASE = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
+    };
+
+    /**
+     * @see {http://stackoverflow.com/a/9855338/3447400}
+     */
+    public static String bytesToStringUppercase(byte[] bytes) {
+        return bytesToStringUppercase(bytes, false /* zeroTerminated */);
+    }
+
+    /**
+     * Encodes a byte array as a hexadecimal representation of bytes.
+     */
+    public static String bytesToStringUppercase(byte[] bytes, boolean zeroTerminated) {
+        int length = bytes.length;
+        StringBuilder out = new StringBuilder(length * 2);
+        for (int i = 0; i < length; i++) {
+            if (zeroTerminated && i == length - 1 && (bytes[i] & 0xff) == 0) {
+                break;
+            }
+            out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
+            out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
+        }
+        return out.toString();
+    }
+
+    /**
+     * @see {http://stackoverflow.com/a/9855338/3447400}
+     */
+    public static String bytesToStringLowercase(byte[] bytes) {
+        char[] hexChars = new char[bytes.length * 2];
+        int j = 0;
+        for (int i = 0; i < bytes.length; i++) {
+            int v = bytes[i] & 0xFF;
+            hexChars[j++] = HEX_LOWERCASE[v >>> 4];
+            hexChars[j++] = HEX_LOWERCASE[v & 0x0F];
+        }
+        return new String(hexChars);
+    }
+
+    public static String bytesToColonDelimitedStringUppercase(byte[] bytes) {
+        if (bytes.length == 0) {
+            return new String();
+        }
+        char[] hexChars = new char[bytes.length * 3 - 1];
+        int j = 0;
+        for (int i = 0; i < bytes.length - 1; i++) {
+            int v = bytes[i] & 0xFF;
+            hexChars[j++] = HEX_UPPERCASE[v >>> 4];
+            hexChars[j++] = HEX_UPPERCASE[v & 0x0F];
+            hexChars[j++] = ':';
+        }
+        int v = bytes[bytes.length - 1] & 0xFF;
+        hexChars[j++] = HEX_UPPERCASE[v >>> 4];
+        hexChars[j++] = HEX_UPPERCASE[v & 0x0F];
+        return new String(hexChars);
+    }
+
+    public static String bytesToColonDelimitedStringLowercase(byte[] bytes) {
+        if (bytes.length == 0) {
+            return new String();
+        }
+        char[] hexChars = new char[bytes.length * 3 - 1];
+        int j = 0;
+        for (int i = 0; i < bytes.length - 1; i++) {
+            int v = bytes[i] & 0xFF;
+            hexChars[j++] = HEX_LOWERCASE[v >>> 4];
+            hexChars[j++] = HEX_LOWERCASE[v & 0x0F];
+            hexChars[j++] = ':';
+        }
+        int v = bytes[bytes.length - 1] & 0xFF;
+        hexChars[j++] = HEX_LOWERCASE[v >>> 4];
+        hexChars[j++] = HEX_LOWERCASE[v & 0x0F];
+        return new String(hexChars);
+    }
+
+    public static byte[] colonDelimitedStringToBytes(String hex) {
+        return stringToBytes(hex.replace(":", ""));
+    }
+
+    public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
+        int length = hex.length();
+        if (length % 2 != 0) {
+            throw new IllegalArgumentException("Hex string has odd number of characters");
+        }
+        byte[] out = new byte[length / 2];
+        for (int i = 0; i < length; i += 2) {
+            // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
+            // our hex values are in 0, 255.
+            out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+        }
+        return out;
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/PlatformVersion.java b/exposurenotification/src/main/java/com/google/samples/PlatformVersion.java
new file mode 100644
index 0000000..11a6e5b
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/PlatformVersion.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples;
+
+import android.os.Build;
+
+public class PlatformVersion {
+    public static boolean isAtLeastM() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
+    }
+
+    public static boolean isAtLeastO() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
+    }
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/Log.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/Log.java
new file mode 100644
index 0000000..ba93b87
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/Log.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Utility class to manage logging of messages with a similar API to systems in Google Play
+ * Services.
+ */
+public class Log {
+    private static final String TAG = "ExposureNotification";
+    public static final Log log = new Log();
+
+    public Logger atVerbose() {
+        return new Logger(android.util.Log::v);
+    }
+
+    public Logger atFine() {
+        return new Logger(android.util.Log::d);
+    }
+
+    public Logger atDebug() {
+        return new Logger(android.util.Log::d);
+    }
+
+    public Logger atInfo() {
+        return new Logger(android.util.Log::i);
+    }
+
+    public Logger atWarning() {
+        return new Logger(android.util.Log::w);
+    }
+
+    public Logger atSevere() {
+        return new Logger(android.util.Log::e);
+    }
+
+    private Log() {
+    }
+
+    public static final class Logger {
+        @NonNull
+        private final LogFunction<String, String, Throwable> out;
+        @Nullable
+        private Throwable cause;
+
+        Logger(@NonNull LogFunction<String, String, Throwable> out) {
+            this.out = out;
+        }
+
+        public Logger withCause(@NonNull Throwable cause) {
+            this.cause = cause;
+            return this;
+        }
+
+        public void log(@NonNull String message) {
+            out.invoke(TAG, message, cause);
+        }
+
+        public void log(@NonNull String format, Object... args) {
+            log(String.format(format, args));
+        }
+    }
+
+    private interface LogFunction<P1, P2, P3> {
+        void invoke(P1 p1, P2 p2, P3 p3);
+    }
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/TekMetadataUtils.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/TekMetadataUtils.java
new file mode 100644
index 0000000..ac56016
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/TekMetadataUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification;
+
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType;
+import com.google.samples.exposurenotification.storage.TekMetadata;
+
+/**
+ * Helpers for working with TEK metadata.
+ */
+public final class TekMetadataUtils {
+
+    /**
+     * Returns true if the metadata's report type is supported by clients. This excludes revoked, for
+     * example.
+     */
+    public static boolean isTekReportTypeReturnableToClients(TekMetadata metadata) {
+        return metadata.getReportType() == ReportType.CONFIRMED_TEST
+                || metadata.getReportType() == ReportType.CONFIRMED_CLINICAL_DIAGNOSIS
+                || metadata.getReportType() == ReportType.SELF_REPORT
+                || metadata.getReportType() == ReportType.RECURSIVE;
+    }
+
+    private TekMetadataUtils() {
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/annotations/Hide.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/annotations/Hide.java
new file mode 100644
index 0000000..0109c11
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/annotations/Hide.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.annotations;
+
+/**
+ * This interface is used only to preserve compatibility with the annotations used in the Exposure
+ * Notifications code, but the actual implementation is not available here because it's not relevant
+ * to the usage of Exposure Notifications.
+ */
+public @interface Hide {
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertisementGenerator.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertisementGenerator.java
new file mode 100644
index 0000000..cf6982e
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertisementGenerator.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.advertising;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.AdvertisementPacket;
+import com.google.samples.exposurenotification.ble.interfaces.BleKeyGenerator;
+import com.google.samples.exposurenotification.crypto.CryptoException;
+import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
+import com.google.samples.exposurenotification.data.BluetoothMetadata;
+import com.google.samples.exposurenotification.data.RollingProximityId;
+import com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataGenerator;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import static com.google.samples.exposurenotification.data.BluetoothMetadata.CALIBRATION_CONFIDENCE_BITS_MASK;
+import static com.google.samples.exposurenotification.data.BluetoothMetadata.CALIBRATION_CONFIDENCE_BITS_OFFSET;
+import static com.google.samples.exposurenotification.data.BluetoothMetadata.VERSION_1_0;
+import static com.google.samples.exposurenotification.data.BluetoothMetadata.VERSION_1_1;
+
+/**
+ * Generates advertisement packet following v1.3 of the BLE spec.
+ */
+public class BleAdvertisementGenerator implements BleKeyGenerator {
+    private final RollingProximityIdManager proxIdManager;
+
+    public BleAdvertisementGenerator(RollingProximityIdManager proxIdManager) {
+        this.proxIdManager = proxIdManager;
+    }
+
+    @Override
+    public AdvertisementPacket generatePacket() throws CryptoException {
+        // byte 0 - version
+        // byte 1 - transmit power level
+        // byte 2 - reserved
+        // byte 3 - reserved
+        return generatePacket(
+                BluetoothMetadata.create(
+                        createVersion(),
+                        (byte)
+                                (ContactTracingFeature.txCalibrationPower()
+                                        + ContactTracingFeature.txCalibrationPowerToTxPower())));
+    }
+
+    @VisibleForTesting
+    AdvertisementPacket generatePacket(BluetoothMetadata metadata) throws CryptoException {
+        RollingProximityId rollingProximityId = proxIdManager.getCurrentRollingProximityId();
+        Log.log.atInfo().log("Generating advertisement with version=%s, tx=%s",
+                metadata.version(), metadata.txPower());
+        // TODO: Consider store AEM generator to avoid recomputation of AEM key for 143 more times.
+        AssociatedEncryptedMetadataGenerator aemGenerator =
+                new AssociatedEncryptedMetadataGenerator(proxIdManager.getTemporaryExposureKey());
+        AssociatedEncryptedMetadata encryptedMetadata =
+                aemGenerator.encrypt(rollingProximityId, metadata);
+
+        return new AdvertisementPacket.Builder()
+                .setProxId(rollingProximityId.getDirect())
+                .setRawMetadata(metadata.toBytes())
+                .setEncryptedMetadata(encryptedMetadata.getDirect())
+                .build();
+    }
+
+    @VisibleForTesting
+    static byte createVersion() {
+        if (ContactTracingFeature.advertisementMetadataV11()) {
+            // bit 7:6 - major version
+            // bit 5:4 - minor version
+            // bit 3:2 - transmit power calibration confidence
+            // bit 1:0 - reserved
+            return (byte)
+                    (VERSION_1_1
+                            | ((ContactTracingFeature.calibrationConfidence()
+                            << CALIBRATION_CONFIDENCE_BITS_OFFSET)
+                            & CALIBRATION_CONFIDENCE_BITS_MASK));
+        } else {
+            // Bits 7:6 = major version (01)
+            // Bits 5:4 = minor version (00)
+            // Bits 3:0 = reserved (0000)
+            return VERSION_1_0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertiser.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertiser.java
new file mode 100644
index 0000000..076f978
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertiser.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.advertising;
+
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSet;
+import android.bluetooth.le.AdvertisingSetCallback;
+import android.bluetooth.le.AdvertisingSetParameters;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Consumer;
+
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.AdvertisementPacket;
+import com.google.samples.exposurenotification.ble.utils.Constants.StatusCode;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import java.util.Objects;
+
+import static com.google.common.base.Verify.verifyNotNull;
+import static com.google.samples.exposurenotification.ble.utils.Constants.CONTACT_TRACER_UUID;
+import static com.google.samples.exposurenotification.ble.utils.Constants.StatusCode.ERROR_BLUETOOTH_UNKNOWN;
+import static com.google.samples.exposurenotification.ble.utils.Constants.StatusCode.OK;
+
+/**
+ * BLE advertiser class. Used to advertising personal ID for contact tracing.
+ */
+public class BleAdvertiser {
+    @Nullable
+    private PlatformBasedAdvertiser advertiser;
+
+    private boolean started = false;
+
+    public BleAdvertiser() {
+    }
+
+    /**
+     * Verifies device currently meets all constraints needed to advertise
+     * FIXME: Implement checks to ensure the device supports BLE Advertising
+     */
+    public StatusCode supportsAdvertising() {
+        return OK;
+    }
+
+    /**
+     * Indicates whether the advertiser is running.
+     */
+    public boolean isAdvertising() {
+        return started;
+    }
+
+    /**
+     * Starts advertising. Subsequent calls to this function cause advertising to restart with the
+     * latest packet.
+     */
+    public StatusCode startAdvertising(AdvertisementPacket packet) {
+        /*
+         * This is the key to ensuring a new, random Bluetooth MAC address is used when rotating
+         * to the next RPI. When advertising is restarted, a new, random Bluetooth MAC address
+         * will be used.
+         */
+        stopAdvertising();
+
+        StatusCode statusCode = supportsAdvertising();
+        if (statusCode != OK) {
+            Log.log.atWarning().log("Advertising not supported, error code=%s", statusCode.name());
+            return statusCode;
+        }
+
+        AdvertiseData data =
+                new AdvertiseData.Builder()
+                        .addServiceUuid(CONTACT_TRACER_UUID)
+                        .addServiceData(CONTACT_TRACER_UUID, packet.getPacket())
+                        .build();
+        PlatformBasedAdvertiser advertiserLocal = advertiser;
+        if (advertiserLocal == null) {
+            advertiserLocal = advertiser = PlatformBasedAdvertiser.getInstance(this::onStartFailure);
+        }
+
+        started = (advertiserLocal != null && advertiserLocal.advertise(data));
+        Log.log.atInfo().log("Start advertising, started=%b", started);
+        return started ? OK : ERROR_BLUETOOTH_UNKNOWN;
+    }
+
+    /**
+     * Stops advertising. Should only be called when user opts out, or contact tracing is no longer
+     * necessary.
+     */
+    public StatusCode stopAdvertising() {
+        StatusCode statusCode = OK;
+        if (started) {
+            // startAdvertising() will ensure that advertiser is non-null
+            // noinspection ConstantConditions
+            boolean stopResult = verifyNotNull(advertiser).stopAdvertising();
+            statusCode = stopResult ? OK : ERROR_BLUETOOTH_UNKNOWN;
+            Log.log.atInfo().log("stopAdvertising get stopResult=%b", stopResult);
+        }
+        resetState();
+        return statusCode;
+    }
+
+    /**
+     * Ensures any internal state is reset if advertising support is lost at any point (BLE turns off)
+     * or if advertising is stopped.
+     */
+    private void resetState() {
+        started = false;
+    }
+
+    private void onStartFailure(int errorCode) {
+        resetState();
+        Log.log.atInfo().log("Start advertising failed with %s", getErrorCodeString(errorCode));
+    }
+
+    private abstract static class PlatformBasedAdvertiser {
+        @Nullable
+        static PlatformBasedAdvertiser getInstance(Consumer<Integer> onFailCallback) {
+            if (VERSION.SDK_INT >= VERSION_CODES.O
+                    && ContactTracingFeature.advertiseUseOreoAdvertiser()) {
+                return new OreoAdvertiser(onFailCallback);
+            } else {
+                return new BaseAdvertiser(onFailCallback);
+            }
+        }
+
+        abstract boolean advertise(AdvertiseData data);
+
+        abstract boolean stopAdvertising();
+    }
+
+    /**
+     * For pre O devices.
+     */
+    private static class BaseAdvertiser extends PlatformBasedAdvertiser {
+        private final ContactTracerAdvertiseCallback advertiseCallback;
+
+        BaseAdvertiser(Consumer<Integer> onFailCallback) {
+            advertiseCallback = new ContactTracerAdvertiseCallback(onFailCallback);
+        }
+
+        @Override
+        boolean advertise(AdvertiseData data) {
+            int advertiseMode = (int) ContactTracingFeature.advertiseMode();
+            int txPowerLevel = (int) ContactTracingFeature.advertiseTxPower();
+            AdvertiseSettings settings =
+                    new AdvertiseSettings.Builder()
+                            .setAdvertiseMode(advertiseMode)
+                            .setTxPowerLevel(txPowerLevel)
+                            .setConnectable(ContactTracingFeature.advertiseConnectable())
+                            .build();
+
+            // FIXME: Call through to BluetoothLeAdvertiser#startAdvertising
+            boolean started = Objects.nonNull(advertiseCallback);
+            if (started) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "Start advertising with packet (%s) mode: %s tx power level %s",
+                                data, getAdvertiseModeString(advertiseMode), getTxPowerLevelString(txPowerLevel));
+            }
+
+            return started;
+        }
+
+        @Override
+        boolean stopAdvertising() {
+            // FIXME: Call through to BluetoothLeAdvertiser#stopAdvertising
+            return Objects.nonNull(advertiseCallback);
+        }
+
+        /**
+         * Notified after attempting to start advertising of whether the advertisement registration was
+         * a success or not.
+         */
+        private static class ContactTracerAdvertiseCallback extends AdvertiseCallback {
+            private final Consumer<Integer> onFailCallback;
+
+            private ContactTracerAdvertiseCallback(Consumer<Integer> onFailCallback) {
+                this.onFailCallback = onFailCallback;
+            }
+
+            @Override
+            public void onStartFailure(int errorCode) {
+                onFailCallback.accept(errorCode);
+            }
+
+            @Override
+            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
+                Log.log.atInfo().log("Start advertising success");
+            }
+        }
+    }
+
+    /**
+     * For O and above devices.
+     */
+    @RequiresApi(api = VERSION_CODES.O)
+    private static class OreoAdvertiser extends PlatformBasedAdvertiser {
+        private final ContactTracerAdvertisingSetCallback advertisingSetCallback;
+
+        OreoAdvertiser(Consumer<Integer> onFailCallback) {
+            advertisingSetCallback = new ContactTracerAdvertisingSetCallback(onFailCallback);
+        }
+
+        @Override
+        boolean advertise(AdvertiseData data) {
+            boolean legacyMode = ContactTracingFeature.advertiseLegacyMode();
+            int advertiseInterval = (int) ContactTracingFeature.advertiseInterval();
+            int txPower = (int) ContactTracingFeature.advertiseTxPowerO();
+            boolean connectable = ContactTracingFeature.advertiseConnectable();
+            boolean scannable = ContactTracingFeature.advertiseScannable();
+            AdvertisingSetParameters parameters =
+                    new AdvertisingSetParameters.Builder()
+                            .setLegacyMode(legacyMode)
+                            .setInterval(advertiseInterval)
+                            .setTxPowerLevel(txPower)
+                            .setConnectable(connectable)
+                            .setScannable(scannable)
+                            .build();
+
+            // FIXME: Call through to BluetoothLeAdvertiser#startAdvertising
+            boolean started = Objects.nonNull(advertisingSetCallback);
+            if (started) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "Start advertising with packet (%s) interval: %d, txPower %d,"
+                                        + " legacyMode=%b, connectable=%b, scannable=%b",
+                                data, advertiseInterval, txPower, legacyMode, connectable, scannable);
+            }
+
+            return started;
+        }
+
+        @Override
+        boolean stopAdvertising() {
+            // FIXME: Call through to BluetoothLeAdvertiser#stopAdvertising
+            return Objects.nonNull(advertisingSetCallback);
+        }
+
+        /**
+         * Notified after attempting to start advertising of whether the advertisement registration was
+         * a success or not.
+         */
+        private static class ContactTracerAdvertisingSetCallback extends AdvertisingSetCallback {
+            private final Consumer<Integer> onFailCallback;
+
+            private ContactTracerAdvertisingSetCallback(Consumer<Integer> onFailCallback) {
+                this.onFailCallback = onFailCallback;
+            }
+
+            @Override
+            public void onAdvertisingSetStarted(AdvertisingSet set, int txPower, int status) {
+                Log.log.atInfo().log("Start advertising,  txPower=%d, status=%d", txPower, status);
+                if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+                    onFailCallback.accept(status);
+                }
+            }
+        }
+    }
+
+    private static String getErrorCodeString(int errorCode) {
+        switch (errorCode) {
+            case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED:
+                return "ALREADY_STARTED";
+            case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE:
+                return "DATA_TOO_LARGE";
+            case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
+                return "FEATURE_UNSUPPORTED";
+            case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR:
+                return "INTERNAL_ERROR";
+            case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
+                return "TOO_MANY_ADVERTISERS";
+            default:
+                return "Unknown code " + errorCode;
+        }
+    }
+
+    private static String getAdvertiseModeString(int advertiseMode) {
+        switch (advertiseMode) {
+            case AdvertiseSettings.ADVERTISE_MODE_LOW_POWER:
+                return "ADVERTISE_MODE_LOW_POWER";
+            case AdvertiseSettings.ADVERTISE_MODE_BALANCED:
+                return "ADVERTISE_MODE_BALANCED";
+            case AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY:
+                return "ADVERTISE_MODE_LOW_LATENCY";
+            default:
+                return "Unknown mode " + advertiseMode;
+        }
+    }
+
+    private static String getTxPowerLevelString(int txPowerLevel) {
+        switch (txPowerLevel) {
+            case AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW:
+                return "ADVERTISE_TX_POWER_ULTRA_LOW";
+            case AdvertiseSettings.ADVERTISE_TX_POWER_LOW:
+                return "ADVERTISE_TX_POWER_LOW";
+            case AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM:
+                return "ADVERTISE_TX_POWER_MEDIUM";
+            case AdvertiseSettings.ADVERTISE_TX_POWER_HIGH:
+                return "ADVERTISE_TX_POWER_HIGH";
+            default:
+                return "Unknown level " + txPowerLevel;
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/RollingProximityIdManager.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/RollingProximityIdManager.java
new file mode 100644
index 0000000..7ccf9e9
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/RollingProximityIdManager.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.advertising;
+
+import androidx.annotation.Nullable;
+import androidx.core.util.Supplier;
+
+import com.google.common.base.Preconditions;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.ble.interfaces.BleDatabaseWriter;
+import com.google.samples.exposurenotification.ble.utils.Constants;
+import com.google.samples.exposurenotification.crypto.CryptoException;
+import com.google.samples.exposurenotification.data.RollingProximityId;
+import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
+import com.google.samples.exposurenotification.data.generator.RollingProximityIdGenerator;
+import com.google.samples.exposurenotification.data.generator.TemporaryExposureKeyGenerator;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+import com.google.samples.exposurenotification.storage.SelfTemporaryExposureKeyDataStore;
+
+import java.time.Instant;
+
+import static com.google.common.base.Verify.verifyNotNull;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+/**
+ * Generate rolling proximity identifiers for BLE advertising, using v1.3.
+ */
+public class RollingProximityIdManager {
+    private final BleDatabaseWriter bleDatabaseWriter;
+    private final Supplier<Long> currentTimeMsSupplier;
+    @Nullable
+    private final SelfTemporaryExposureKeyDataStore selfTemporaryExposureKeyDataStore;
+    private int latestEnIntervalNumber;
+    @Nullable
+    private TemporaryExposureKey temporaryExposureKey;
+    @Nullable
+    private RollingProximityId latestRollingProximityId;
+
+    public RollingProximityIdManager(
+            @Nullable SelfTemporaryExposureKeyDataStore selfTemporaryExposureKeyDataStore,
+            BleDatabaseWriter bleDatabaseWriter,
+            Supplier<Long> currentTimeMsSupplier) {
+        this.selfTemporaryExposureKeyDataStore = selfTemporaryExposureKeyDataStore;
+        this.bleDatabaseWriter = bleDatabaseWriter;
+        this.currentTimeMsSupplier = currentTimeMsSupplier;
+    }
+
+    public RollingProximityIdManager(
+            @Nullable SelfTemporaryExposureKeyDataStore selfTemporaryExposureKeyDataStore,
+            BleDatabaseWriter bleDatabaseWriter,
+            Supplier<Long> currentTimeMsSupplier,
+            TemporaryExposureKey temporaryExposureKey) {
+        this.selfTemporaryExposureKeyDataStore = selfTemporaryExposureKeyDataStore;
+        this.bleDatabaseWriter = bleDatabaseWriter;
+        this.currentTimeMsSupplier = currentTimeMsSupplier;
+        this.temporaryExposureKey = temporaryExposureKey;
+    }
+
+    /**
+     * Gets the rolling proximity id for current time (given instant).
+     */
+    public synchronized RollingProximityId getCurrentRollingProximityId() throws CryptoException {
+        Instant instant = Instant.ofEpochMilli(currentTimeMsSupplier.get());
+        int currentEnIntervalNumber = getCurrentEnIntervalNumber(instant);
+        if (latestRollingProximityId != null && currentEnIntervalNumber == latestEnIntervalNumber) {
+            Log.log
+                    .atInfo()
+                    .log(
+                            "getCurrentRollingProximityId: same latestEnIntervalNumber=%d",
+                            latestEnIntervalNumber);
+            return verifyNotNull(latestRollingProximityId);
+        }
+
+        Log.log
+                .atInfo()
+                .log(
+                        "getCurrentRollingProximityId: current/latestEnIntervalNumber=%d/%d",
+                        currentEnIntervalNumber, latestEnIntervalNumber);
+        //
+        if (temporaryExposureKey == null && selfTemporaryExposureKeyDataStore != null) {
+            TemporaryExposureKey existingKey =
+                    selfTemporaryExposureKeyDataStore.getSingleKeyForIntervalNumber(currentEnIntervalNumber);
+            // Keep the nullness checker happy, by never assigning null to temporaryExposureKey.
+            if (existingKey != null) {
+                temporaryExposureKey = existingKey;
+            }
+        }
+        if (temporaryExposureKey == null
+                || currentEnIntervalNumber
+                >= TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey)) {
+            temporaryExposureKey =
+                    TemporaryExposureKeyGenerator.generateKey(
+                            getAlignedEnIntervalNumber(currentEnIntervalNumber));
+            Log.log.atInfo().log("getCurrentRollingProximityId: generated a new Temporary Exposure Key");
+            Log.log
+                    .atVerbose()
+                    .log(
+                            "getCurrentRollingProximityId: TEK=%s",
+                            Constants.fromIdByteArrayToString(verifyNotNull(temporaryExposureKey).getKeyData()));
+            bleDatabaseWriter.writeNewTemporaryExposureKey(verifyNotNull(temporaryExposureKey));
+        }
+
+        verifyNotNull(temporaryExposureKey);
+
+        // Generates a new Rolling Proximity Id.
+        latestEnIntervalNumber = currentEnIntervalNumber;
+        latestRollingProximityId =
+                new RollingProximityIdGenerator(
+                        temporaryExposureKey.getKeyData(),
+                        temporaryExposureKey.getRollingStartIntervalNumber(),
+                        TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey),
+                        (int) ContactTracingFeature.rollingProximityIdKeySizeBytes(),
+                        ContactTracingFeature.rpikHkdfInfoString(),
+                        ContactTracingFeature.rpidAesPaddedString())
+                        .generateId(latestEnIntervalNumber);
+        Log.log
+                .atInfo()
+                .log(
+                        "getCurrentRollingProximityId: generated a new RollingProximityId=%s",
+                        latestRollingProximityId);
+        return verifyNotNull(latestRollingProximityId);
+    }
+
+    synchronized TemporaryExposureKey getTemporaryExposureKey() {
+        // getCurrentRollingProximityId() must always be called before this method
+        Preconditions.checkNotNull(temporaryExposureKey, "Must call getCurrentRollingProximityId()");
+        return temporaryExposureKey;
+    }
+
+    private static int getCurrentEnIntervalNumber(Instant instant) {
+        // TODO: Make sure using ceil or floor.
+        return (int) (instant.toEpochMilli() /
+                MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes()));
+    }
+
+    private static int getAlignedEnIntervalNumber(int enIntervalNumber) {
+        int tkRollingPeriod = (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
+        return tkRollingPeriod * (enIntervalNumber / tkRollingPeriod);
+    }
+
+    public synchronized void clearCache() {
+        Log.log.atInfo().log("RollingProximityIdManager.clearCache");
+        latestEnIntervalNumber = 0;
+        temporaryExposureKey = null;
+        latestRollingProximityId = null;
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/data/RollingProximityIdValidator.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/data/RollingProximityIdValidator.java
new file mode 100644
index 0000000..52b8ee1
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/data/RollingProximityIdValidator.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.data;
+
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.data.GeneratedRollingProximityId;
+import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import org.joda.time.Duration;
+
+/**
+ * Utility method to validate whether a sighting falls within a valid time window.
+ *
+ * <p>A valid sighting falls within {@link
+ * ContactTracingFeature#tkMatchingClockDriftRollingPeriods()} of the start and end of a {@link
+ * GeneratedRollingProximityId#intervalNumber()}
+ */
+public class RollingProximityIdValidator {
+
+    /** Returns true if the sighting is within the expected time bounds. */
+    public static boolean isSightingValid(
+            GeneratedRollingProximityId generatedId,
+            Duration sightingTimeSinceEpoch,
+            TemporaryExposureKey diagnosisKey) {
+        int sightingTimeIntervalNumber =
+                TimeIntervalNumberUtility.getTimeIntervalNumber(sightingTimeSinceEpoch);
+        return sightingTimeIntervalNumber >= getValidWindowStartIntervalNumber(generatedId)
+                && sightingTimeIntervalNumber < getValidWindowEndIntervalNumber(generatedId, diagnosisKey);
+    }
+
+    /**
+     * Gets the start of the valid time window based on {@link
+     * GeneratedRollingProximityId#intervalNumber()}.
+     */
+    public static int getValidWindowStartIntervalNumber(GeneratedRollingProximityId generatedId) {
+        return (int)
+                (generatedId.intervalNumber() - ContactTracingFeature.tkMatchingClockDriftRollingPeriods());
+    }
+
+    /**
+     * Gets the exclusive end of the valid time window based on {@link
+     * GeneratedRollingProximityId#intervalNumber()}. Clock drift will only be considered if the
+     * resulting validEndIntervalNumber falls within the provided {@code diagnosisKey}'s validity
+     * period. If the computed validStartIntervalNumber results in an ending interval later than the
+     * provided {@code diagnosisKey}, then the {@code diagnosisKey}'s end interval is returned
+     * instead.
+     */
+    public static int getValidWindowEndIntervalNumber(
+            GeneratedRollingProximityId generatedId, TemporaryExposureKey diagnosisKey) {
+        int windowEndIntervalNumber =
+                (int)
+                        (generatedId.intervalNumber()
+                                + ContactTracingFeature.tkMatchingClockDriftRollingPeriods()
+                                + 1);
+        int keyEndIntervalNumber =
+                TemporaryExposureKeySupport.getRollingEndIntervalNumber(diagnosisKey);
+        if (ContactTracingFeature.ignoreEmbargoPeriodWhenMatchingNearKeyEdges()
+                && windowEndIntervalNumber > keyEndIntervalNumber) {
+            windowEndIntervalNumber = keyEndIntervalNumber;
+        }
+        return windowEndIntervalNumber;
+    }
+    private RollingProximityIdValidator() {}
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/data/TimeIntervalNumberUtility.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/data/TimeIntervalNumberUtility.java
new file mode 100644
index 0000000..8c41b6d
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/data/TimeIntervalNumberUtility.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.data;
+
+import com.google.samples.Clock.DefaultClock;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import org.joda.time.Duration;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+/**
+ * A utility class for methods used to convert time interval numbers to time and vice versa.
+ */
+public class TimeIntervalNumberUtility {
+    /**
+     * Converts the current time to a time interval number. Using DefaultClock for testability
+     */
+    public static int getCurrentIntervalNumber() {
+        return (int)
+                (DefaultClock.getInstance().currentTimeMillis()
+                        / MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes()));
+    }
+
+    /**
+     * Converts the passed in time since epoch into a time interval number, based on the feature flag
+     * values that are provided.
+     */
+    public static int getTimeIntervalNumber(Duration timeSinceEpoch) {
+        return (int)
+                (timeSinceEpoch.getStandardMinutes() / ContactTracingFeature.idRollingPeriodMinutes());
+    }
+
+    /**
+     * Gets the latest {@link TemporaryExposureKey#getRollingStartIntervalNumber()}} from before the
+     * provided time.
+     */
+    public static int getLatestStartIntervalNumber(Duration timeSinceEpoch) {
+        int numKeysSinceEpoch =
+                (int)
+                        (numIntervalsPer(timeSinceEpoch)
+                                / ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
+        return numKeysSinceEpoch
+                * (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
+    }
+
+    /**
+     * Returns the number of intervals passed wtihin a given {@code duration}.
+     */
+    public static int numIntervalsPer(Duration duration) {
+        return (int) (duration.getStandardMinutes() / ContactTracingFeature.idRollingPeriodMinutes());
+    }
+
+    /**
+     * Calculates milliseconds since epoch from {@code intervalNumber}.
+     */
+    public static long getMillisSinceEpoch(int intervalNumber) {
+        return Duration.standardMinutes(intervalNumber * ContactTracingFeature.idRollingPeriodMinutes())
+                .getMillis();
+    }
+
+    private TimeIntervalNumberUtility() {
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleDatabaseWriter.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleDatabaseWriter.java
new file mode 100644
index 0000000..5f519e5
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleDatabaseWriter.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.interfaces;
+
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+
+/**
+ * Stores BLE scanning and advertising history to database.
+ */
+public interface BleDatabaseWriter {
+    /**
+     * Writes sighting contact tracing ID into database.
+     */
+    void writeBleSighting(
+            byte[] rollingProximityId,
+            int rssi,
+            byte[] associatedEncryptedMetadata,
+            int previousScanEpochSeconds);
+
+    /**
+     * Writes new generated contact tracing ID into database.
+     */
+    void writeNewTemporaryExposureKey(TemporaryExposureKey temporaryExposureKey);
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleExecutors.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleExecutors.java
new file mode 100644
index 0000000..4c8de04
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleExecutors.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.interfaces;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides executor for BLE scanning and advertising.
+ */
+public interface BleExecutors {
+    /**
+     * Schedules a {@code command} with specific delay.
+     *
+     * @return the canceller of this scheduled task
+     */
+    ScheduledTask schedule(Runnable command, long delay, TimeUnit unit);
+
+    /**
+     * Schedules a {@code command} with specific delay with {@link ScheduledExecutorService}.
+     *
+     * @return the canceller of this scheduled task
+     */
+    ScheduledTask scheduleWithScheduledExecutor(Runnable command, long delay, TimeUnit unit);
+
+    /**
+     * Schedules a {@code command} with specific delay. OpportunisticSchedule will not be executed
+     * exactly on time, but will be checked everytime when {@link BleExecutors#schedule(Runnable,
+     * long, TimeUnit)} called or its scheduled task fired. If current time is larger than target time
+     * when checking, the opportunisticSchedule will be executed.
+     */
+    ScheduledTask opportunisticSchedule(Runnable runnable, long delay, TimeUnit unit);
+
+    void start();
+
+    void stop();
+
+    /**
+     * Allows scheduled runnable to be cancelled, and also provides status check for cancellation.
+     */
+    interface ScheduledTask {
+        boolean isCancelable();
+
+        boolean cancel();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleKeyGenerator.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleKeyGenerator.java
new file mode 100644
index 0000000..2ecd133
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleKeyGenerator.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.interfaces;
+
+import com.google.samples.exposurenotification.nearby.AdvertisementPacket;
+import com.google.samples.exposurenotification.crypto.CryptoException;
+
+/**
+ * Interface used to generate rotating proximity ID for BLE advertisment.
+ */
+public interface BleKeyGenerator {
+
+    /**
+     * Generates a new AdvertisementPacket according to
+     * https://covid19-static.cdn-apple.com/applications/covid19/current/static/contact-tracing/pdf/ExposureNotification-CryptographySpecificationv1.1.pdf
+     */
+    AdvertisementPacket generatePacket() throws CryptoException;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleScanner.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleScanner.java
new file mode 100644
index 0000000..eb6969e
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/interfaces/BleScanner.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.interfaces;
+
+import com.google.samples.exposurenotification.ble.utils.Constants.StatusCode;
+
+/**
+ * BLE scanner interface. Used to scan nearby BLE advertisements for contact tracing.
+ */
+public interface BleScanner {
+    /**
+     * Checks if current device is able to scan.
+     */
+    StatusCode supportScanning();
+
+    /**
+     * Starts advertising. Should only be called when user opt in, or reboot completed.
+     */
+    StatusCode startScanning();
+
+    /**
+     * Starts advertising. Should only be called when user opt out, or contact tracing is no longer
+     * necessary.
+     */
+    StatusCode stopScanning();
+
+    /**
+     * Indicates current scanner's state is scanning or not.
+     */
+    boolean isScanning();
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/scanner/BleScannerImpl.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/scanner/BleScannerImpl.java
new file mode 100644
index 0000000..e78c2f0
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/scanner/BleScannerImpl.java
@@ -0,0 +1,729 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.scanner;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanFilter.Builder;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.bluetooth.le.ScanSettings;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build.VERSION_CODES;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.WorkSource;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.arch.core.util.Function;
+import androidx.core.util.Supplier;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.samples.PlatformVersion;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.ble.interfaces.BleDatabaseWriter;
+import com.google.samples.exposurenotification.ble.interfaces.BleExecutors;
+import com.google.samples.exposurenotification.ble.interfaces.BleExecutors.ScheduledTask;
+import com.google.samples.exposurenotification.ble.interfaces.BleScanner;
+import com.google.samples.exposurenotification.ble.utils.BluetoothUtils;
+import com.google.samples.exposurenotification.ble.utils.Constants;
+import com.google.samples.exposurenotification.ble.utils.Constants.StatusCode;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.google.samples.exposurenotification.ble.utils.Constants.StatusCode.ERROR_BLUETOOTH_UNKNOWN;
+import static com.google.samples.exposurenotification.ble.utils.Constants.StatusCode.OK;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * Implementation of {@link BleScanner}, used to scan BLE advertisement for contact tracing.
+ */
+@TargetApi(VERSION_CODES.LOLLIPOP)
+@SuppressWarnings("MissingPermission")
+public class BleScannerImpl implements BleScanner {
+    private static final String WAKE_LOCK_TAG = "nearby:ExposureNotificationScanner";
+    private static final int MAX_RSSI_VALUE = 126;
+    private static final int MIN_RSSI_VALUE = -127;
+    private static final ImmutableList<Integer> CHECKING_PROFILES =
+            ImmutableList.of(BluetoothProfile.A2DP, BluetoothProfile.HEADSET);
+    private static final int SCAN_CHANNEL_NUMBER = 3;
+
+    private final Context context;
+    private final BleExecutors bleExecutors;
+    private final Supplier<Long> currentTimeMsSupplier;
+    private final BleSightingCallback bleSightingCallback;
+    private final AudioManager audioManager;
+    private final PowerManager powerManager;
+    @VisibleForTesting
+    final ScanStatusManager scanStatusManager;
+    private final ScanTimePersistence scanTimePersistence;
+
+    @Nullable
+    @VisibleForTesting
+    ScheduledTask scheduledTask;
+    @Nullable
+    private ScheduledTask piggybackScheduledTask;
+    private long lastScheduleTimeMsForStartScan;
+    /**
+     * Master WakeLock that keeps the cpu awake while scanning.
+     */
+    private final WakeLock wakeLock;
+
+    @Nullable
+    private WorkSource workSource;
+
+    private Function<Integer, Integer> randomFunction =
+            integer -> new SecureRandom().nextInt(integer);
+    private Predicate<List<Integer>> profileInUsingPredicator =
+            BluetoothUtils::isBluetoothProfileInUsing;
+
+    private int defaultScanWindowMs;
+    private int defaultScanIntervalMs;
+
+    // This object will be fully initialized before rescheduleScanForOpportunisticScan() is called
+    // incompatible types in assignment.
+    public BleScannerImpl(
+            Context context,
+            BleDatabaseWriter databaseWriter,
+            BleExecutors bleExecutors,
+            Supplier<Long> currentTimeMsSupplier,
+            Runnable startScanFailCallback) {
+        this.context = context;
+        this.bleExecutors = bleExecutors;
+        this.currentTimeMsSupplier = currentTimeMsSupplier;
+        this.scanTimePersistence = new ScanTimePersistence();
+        this.scanStatusManager = new ScanStatusManager();
+        this.bleSightingCallback =
+                new BleSightingCallback(
+                        databaseWriter,
+                        () -> rescheduleScanForOpportunisticScan(/* ignoreFrequency= */ false),
+                        scanStatusManager,
+                        scanTimePersistence,
+                        startScanFailCallback);
+        this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        this.powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        // FIXME: Aquire a WakeLock
+        this.wakeLock = null;
+        this.defaultScanIntervalMs = getCurrentSettingsGlobalScanInterval();
+        this.defaultScanWindowMs = getCurrentSettingsGlobalScanWindow();
+    }
+
+    /**
+     * Verifies the device supports BLE scanning.
+     * FIXME: Implement checks to ensure the device supports BLE Advertising
+     */
+    @Override
+    public StatusCode supportScanning() {
+        return OK;
+    }
+
+    @Override
+    public boolean isScanning() {
+        return !scanStatusManager.isStopped();
+    }
+
+    @VisibleForTesting
+    @Nullable
+    WorkSource getWorkSource() {
+        return workSource;
+    }
+
+    @Override
+    public StatusCode startScanning() {
+        StatusCode statusCode = supportScanning();
+        if (statusCode != OK) {
+            return statusCode;
+        }
+
+        if (isScanning()) {
+            return OK;
+        }
+
+        setActiveClientAsWorkSource();
+        return startScanningInternal(/* isPiggyBackScan= */ false) ? OK : ERROR_BLUETOOTH_UNKNOWN;
+    }
+
+    private void setActiveClientAsWorkSource() {
+        if (workSource != null) {
+            // FIXME: Remove old WorkSource if needed
+            workSource = null;
+        }
+
+        // FIXME: Get record of calling package
+        String callingPackage = "com.google.android.apps.exposurenotification";
+        // FIXME: Create a meaningful WorkSource
+        workSource = new WorkSource();
+        if (workSource != null) {
+            wakeLock.setWorkSource(workSource);
+            Log.log.atInfo().log("BleScanner set work source to %s", callingPackage);
+        }
+    }
+
+    private synchronized boolean startScanningInternal(boolean isPiggyBackScan) {
+        if (scanStatusManager.isScanning()) {
+            Log.log.atInfo().log("Already scanning!");
+            return false;
+        }
+        cancelAllScheduleTasks();
+
+        maybeUpdateBleScannerInstance();
+
+        scanStatusManager.setScanning();
+        int scanMode = (int) ContactTracingFeature.scanMode();
+        ScanSettings.Builder builder = new ScanSettings.Builder();
+        builder.setScanMode(scanMode);
+        if (PlatformVersion.isAtLeastM()) {
+            int callbackType = ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
+            int matchMode = ScanSettings.MATCH_MODE_STICKY;
+            builder.setCallbackType(callbackType).setMatchMode(matchMode);
+            if (PlatformVersion.isAtLeastO()) {
+                builder.setLegacy(ContactTracingFeature.scanOnlyLegacy());
+                Log.log
+                        .atInfo()
+                        .log("Set scanning parameters. setLegacy=%b", ContactTracingFeature.scanOnlyLegacy());
+            }
+            Log.log
+                    .atInfo()
+                    .log("Set scanning parameters. matchMode=%d, callback type=%d", matchMode, callbackType);
+        }
+
+        int durationSeconds = getScanTime(isPiggyBackScan);
+
+        // Set new scanning parameters allowing us to more closely control which channels we're using.
+        // The channel is only updated when the window value passes, meaning that with the default
+        // implementation we will scan for 4s on channel 37, then 4s on channel 38, and 4s on channel
+        // 39 before returning to 37. By shrinking the window, we can cause scanning to cycle through
+        // all 3 channels over the course of our EN scan.
+        defaultScanWindowMs = getCurrentSettingsGlobalScanWindow();
+        defaultScanIntervalMs = getCurrentSettingsGlobalScanInterval();
+        int scanChannelTimeMs = (durationSeconds * 1000) / SCAN_CHANNEL_NUMBER;
+        setScanningSettingsGlobalParameters(scanChannelTimeMs, scanChannelTimeMs);
+
+        ScanFilter filter = new Builder()
+                .setServiceData(Constants.CONTACT_TRACER_UUID, new byte[]{0}, new byte[]{0})
+                .build();
+        // FIXME: Call through to BluetoothLeScanner#startScan with the `filter` declared above
+        //  and the bleSightingCallback callback.
+        boolean result = true;
+        Log.log.atInfo().log("Starting scanning. scanMode=%d", scanMode);
+        if (durationSeconds > 0) {
+            Log.log.atInfo().log("Schedule stop the scan after %d seconds", durationSeconds);
+            scheduledTask =
+                    bleExecutors.schedule(
+                            () -> {
+                                if (PlatformVersion.isAtLeastM()
+                                        && ContactTracingFeature.enableOpportunisticScan()) {
+                                    stopScanningInternal();
+                                    scanStatusManager.setPaused();
+                                    startOpportunisticScanningInternal();
+                                } else {
+                                    pauseScanning();
+                                }
+                            },
+                            durationSeconds,
+                            SECONDS);
+            if (ContactTracingFeature.wakelockForScan()) {
+                Log.log.atInfo().log("Hold wakelock %d seconds for scanning", durationSeconds + 1);
+                wakeLock.acquire(SECONDS.toMillis(durationSeconds + 1));
+            }
+        }
+        return result;
+    }
+
+    @RequiresApi(VERSION_CODES.M)
+    private synchronized boolean startOpportunisticScanningInternal() {
+        if (scanStatusManager.isOpportunisticScanning()) {
+            Log.log.atInfo().log("Already opportunistic scanning!");
+            return false;
+        }
+
+        maybeUpdateBleScannerInstance();
+
+        scanStatusManager.setOpportunisticScanning();
+        ScanSettings.Builder builder =
+                new ScanSettings.Builder()
+                        .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC)
+                        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
+                        .setMatchMode(ScanSettings.MATCH_MODE_STICKY);
+        if (PlatformVersion.isAtLeastO()) {
+            builder.setLegacy(ContactTracingFeature.scanOnlyLegacy());
+        }
+
+        ScanFilter filter = new Builder()
+                .setServiceData(Constants.CONTACT_TRACER_UUID, new byte[]{0}, new byte[]{0})
+                .build();
+        // FIXME: Call through to BluetoothLeScanner#startScan with the `filter` declared above
+        //  and the bleSightingCallback callback.
+        boolean result = true;
+        Log.log.atInfo().log("Starting opportunistic scanning.");
+        rescheduleScanForOpportunisticScan(/* ignoreFrequency= */ true);
+        return result;
+    }
+
+    private void rescheduleScanForOpportunisticScan(boolean ignoreFrequency) {
+        if (!scanStatusManager.isOpportunisticScanning()) {
+            return;
+        }
+        if (!ignoreFrequency) {
+            long timeMsSinceLastSchedule = currentTimeMsSupplier.get() - lastScheduleTimeMsForStartScan;
+            if (timeMsSinceLastSchedule < ContactTracingFeature.rescheduleStartScanMinIntervalMs()) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "Reschedule interval too short, timeMsSinceLastSchedule=%d",
+                                timeMsSinceLastSchedule);
+                return;
+            }
+        }
+
+        rescheduleStartScan();
+        lastScheduleTimeMsForStartScan = currentTimeMsSupplier.get();
+    }
+
+    private void rescheduleStartScan() {
+        cancelAllScheduleTasks();
+
+        int intervalSeconds = getScanInterval();
+        Log.log.atInfo().log("Schedule start normal scan after %d seconds", intervalSeconds);
+        scheduledTask =
+                bleExecutors.schedule(
+                        () -> restartScanByScheduledTask(/* isPiggyBackScan= */ false),
+                        intervalSeconds,
+                        SECONDS);
+        schedulePiggybackScan();
+    }
+
+    private synchronized void schedulePiggybackScan() {
+        if (!ContactTracingFeature.enablePiggybackScan()) {
+            return;
+        }
+
+        long startTime = currentTimeMsSupplier.get();
+        int piggyBackScanMinIntervalSeconds =
+                (int) ContactTracingFeature.piggybackScanIntervalSeconds();
+        int piggybackScanDelaySeconds =
+                (PlatformVersion.isAtLeastM() && powerManager.isDeviceIdleMode())
+                        ? (int) ContactTracingFeature.piggybackScanDelayWhileDeviceIdleSeconds()
+                        : powerManager.isInteractive()
+                        ? piggyBackScanMinIntervalSeconds
+                        : (int) ContactTracingFeature.piggybackScanDelayWhileScreenOffSeconds();
+        Log.log.atInfo().log("Try to piggyback scan after %d seconds", piggybackScanDelaySeconds);
+        piggybackScheduledTask =
+                bleExecutors.scheduleWithScheduledExecutor(
+                        () -> {
+                            int actualDelay =
+                                    (int) MILLISECONDS.toSeconds(currentTimeMsSupplier.get() - startTime);
+                            if (actualDelay - piggyBackScanMinIntervalSeconds
+                                    < ContactTracingFeature.piggybackAccetableMinDelaySeconds()) {
+                                Log.log
+                                        .atInfo()
+                                        .log(
+                                                "Ignored piggyback task, actually delayed %ds is too short, cpu might"
+                                                        + " still awake",
+                                                actualDelay);
+                                schedulePiggybackScan();
+                                return;
+                            }
+
+                            Log.log.atInfo().log("Executing piggyback task, actually delayed %ds", actualDelay);
+                            restartScanByScheduledTask(/* isPiggyBackScan= */ true);
+                        },
+                        piggybackScanDelaySeconds,
+                        SECONDS);
+    }
+
+    private synchronized void restartScanByScheduledTask(boolean isPiggyBackScan) {
+        if (scanStatusManager.isScanning()) {
+            Log.log.atInfo().log("Already scanning, skip restartScanByScheduledTask");
+            return;
+        }
+        Log.log.atInfo().log("restartScanByScheduledTask prepare to restart scan");
+        stopScanningInternal();
+        scanStatusManager.setPaused();
+        startScanningInternal(isPiggyBackScan);
+    }
+
+    private synchronized void cancelAllScheduleTasks() {
+        if (scheduledTask != null) {
+            scheduledTask.cancel();
+            scheduledTask = null;
+        }
+        if (piggybackScheduledTask != null) {
+            piggybackScheduledTask.cancel();
+            piggybackScheduledTask = null;
+        }
+    }
+
+    @Override
+    public StatusCode stopScanning() {
+        Log.log.atInfo().log("User turn off scanning.");
+        cancelAllScheduleTasks();
+
+        boolean result = stopScanningInternal();
+        if (scanStatusManager.isPaused()) {
+            result = true;
+        }
+        scanStatusManager.setStopped();
+        return result ? OK : ERROR_BLUETOOTH_UNKNOWN;
+    }
+
+    private synchronized void pauseScanning() {
+        stopScanningInternal();
+        scanStatusManager.setPaused();
+        rescheduleStartScan();
+    }
+
+    private synchronized boolean stopScanningInternal() {
+        try {
+            if (!scanStatusManager.isScanning() && !scanStatusManager.isOpportunisticScanning()) {
+                Log.log.atInfo().log("Not scanning!");
+                return false;
+            }
+
+            Log.log.atInfo().log("Stopping scanning.");
+            scanTimePersistence.markLastScanStopTime();
+
+            // Reset the scanning parameters back to their default values. This is so that other apps also
+            // performing scanning aren't affected long term by the updated values set during
+            // startScanningInternal.
+            setScanningSettingsGlobalParameters(defaultScanWindowMs, defaultScanIntervalMs);
+
+            // FIXME: Call BluetoothLeScanner#stopScan with bleSightingCallback
+            return true;
+        } finally {
+            if (wakeLock.isHeld()) {
+                wakeLock.release();
+            }
+        }
+    }
+
+    private void maybeUpdateBleScannerInstance() {
+        // FIXME: Get a reference to a BluetoothLeScanner if necessary
+    }
+
+    private int getScanTime(boolean isPiggyBackScan) {
+        if (ContactTracingFeature.scanIntervalSeconds() <= 0) {
+            return 0;
+        }
+
+        int scanTime =
+                (int)
+                        (isPiggyBackScan
+                                ? ContactTracingFeature.piggybackScanTimeSeconds()
+                                : ContactTracingFeature.scanTimeSeconds());
+
+        if (needToExtendScanTime()) {
+            Log.log.atInfo().log("Extend the scan time because bluetooth is in using.");
+            return (int) (scanTime + ContactTracingFeature.scanTimeExtendForProfileInUseSeconds());
+        }
+        return scanTime;
+    }
+
+    private boolean needToExtendScanTime() {
+        if (ContactTracingFeature.extendScanOnlyForBtAudioOrCalls()) {
+            if (audioManager.isBluetoothScoOn()) {
+                Log.log.atInfo().log("Using bluetooth device for communication");
+                return true;
+            } else if (audioManager.isMusicActive() && audioManager.isBluetoothA2dpOn()) {
+                Log.log.atInfo().log("Using bluetooth device for streaming audio");
+                return true;
+            }
+            return false;
+        }
+
+        if (profileInUsingPredicator.apply(CHECKING_PROFILES)) {
+            Log.log.atInfo().log("Extend the san time because some bt profiles is in using.");
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    @VisibleForTesting
+    void setProfileInUsingPredicator(Predicate<List<Integer>> profileInUsingPredicator) {
+        this.profileInUsingPredicator = profileInUsingPredicator;
+    }
+
+    private int getScanInterval() {
+        if (ContactTracingFeature.scanIntervalSeconds() <= 0) {
+            return 1;
+        }
+
+        int scanIntervalSeconds = (int) ContactTracingFeature.scanIntervalSeconds();
+        int scanRandomRange = (int) ContactTracingFeature.scanIntervalRandomRangeSeconds();
+
+        if (scanRandomRange >= scanIntervalSeconds) {
+            scanRandomRange = scanIntervalSeconds - 1;
+        }
+
+        scanIntervalSeconds =
+                scanIntervalSeconds - (scanRandomRange > 0 ? randomFunction.apply(scanRandomRange) : 0);
+
+        if (powerManager.isInteractive()) {
+            scanIntervalSeconds =
+                    (int) (scanIntervalSeconds * ContactTracingFeature.scanIntervalRatioWhenScreenOn());
+            Log.log
+                    .atInfo()
+                    .log(
+                            "BleScanner adjust scan interval due to screen on, adjust ratio %.2f",
+                            ContactTracingFeature.scanIntervalRatioWhenScreenOn());
+        }
+
+        if (audioManager.isMusicActive()) {
+            scanIntervalSeconds =
+                    (int) (scanIntervalSeconds * ContactTracingFeature.scanIntervalRatioWhenAudioPlayback());
+            Log.log
+                    .atInfo()
+                    .log(
+                            "BleScanner adjust scan interval due to audio playback, adjust ratio %.2f",
+                            ContactTracingFeature.scanIntervalRatioWhenAudioPlayback());
+        }
+
+        int minInterval =
+                Math.min(
+                        (int) ContactTracingFeature.minScanIntervalSeconds(),
+                        (int) ContactTracingFeature.scanIntervalSeconds());
+        if (scanIntervalSeconds < minInterval) {
+            scanIntervalSeconds = minInterval;
+        }
+        return scanIntervalSeconds;
+    }
+
+    @VisibleForTesting
+    int getCurrentSettingsGlobalScanWindow() {
+        return getCurrentSettingsGlobalScanParameter(
+                ContactTracingFeature.lowLatencyScanWindowSettingsName(),
+                (int) ContactTracingFeature.lowLatencyScanWindowDefaultValueMillis());
+    }
+
+    @VisibleForTesting
+    int getCurrentSettingsGlobalScanInterval() {
+        return getCurrentSettingsGlobalScanParameter(
+                ContactTracingFeature.lowLatencyScanIntervalSettingsName(),
+                (int) ContactTracingFeature.lowLatencyScanIntervalDefaultValueMillis());
+    }
+
+    private int getCurrentSettingsGlobalScanParameter(String name, int defaultValue) {
+        return Settings.Global.getInt(context.getContentResolver(), name, defaultValue);
+    }
+
+    private void setScanningSettingsGlobalParameters(int scanWindow, int scanInterval) {
+        if (ContactTracingFeature.bleScanningAdjustIntervalAndWindowValues()) {
+            ContentResolver resolver = context.getContentResolver();
+            Settings.Global.putInt(
+                    resolver, ContactTracingFeature.lowLatencyScanWindowSettingsName(), scanWindow);
+            Settings.Global.putInt(
+                    resolver, ContactTracingFeature.lowLatencyScanIntervalSettingsName(), scanInterval);
+        }
+    }
+
+    @VisibleForTesting
+    void setRandomFunction(Function<Integer, Integer> randomFunction) {
+        this.randomFunction = randomFunction;
+    }
+
+    private static class BleSightingCallback extends ScanCallback {
+        private final BleDatabaseWriter databaseWriter;
+        private final Runnable rescheduler;
+        private final ScanTimePersistence scanTimePersistence;
+        private final ScanStatusManager scanStatusManager;
+        private final Runnable startScanFailCallback;
+
+        public BleSightingCallback(
+                BleDatabaseWriter databaseWriter,
+                Runnable rescheduler,
+                ScanStatusManager scanStatusManager,
+                ScanTimePersistence scanTimePersistence,
+                Runnable startScanFailCallback) {
+            this.databaseWriter = databaseWriter;
+            this.rescheduler = rescheduler;
+            this.scanStatusManager = scanStatusManager;
+            this.scanTimePersistence = scanTimePersistence;
+            this.startScanFailCallback = startScanFailCallback;
+        }
+
+        @Override
+        public void onScanResult(int callbackType, ScanResult scanResult) {
+            ScanRecord scanRecord;
+            if (scanResult == null
+                    || (scanRecord = scanResult.getScanRecord()) == null
+                    || scanRecord.getServiceData() == null) {
+                Log.log.atWarning().log("Empty scanResult or getScanRecord, skipping.");
+                return;
+            }
+            byte[] serviceData = scanRecord.getServiceData(Constants.CONTACT_TRACER_UUID);
+            if (serviceData == null || serviceData.length <= 0) {
+                Log.log.atWarning().log("Empty service data, skipping.");
+                return;
+            }
+
+            if (serviceData.length < ContactTracingFeature.contactIdLength()) {
+                Log.log.atWarning().log("Invalid ID:%s", Constants.fromIdByteArrayToString(serviceData));
+                return;
+            }
+
+            byte[] idByte = Arrays.copyOf(serviceData, (int) ContactTracingFeature.contactIdLength());
+            String id = Constants.fromIdByteArrayToString(idByte);
+            byte[] metadata =
+                    serviceData.length > ContactTracingFeature.contactIdLength()
+                            ? Arrays.copyOfRange(
+                            serviceData, (int) ContactTracingFeature.contactIdLength(), serviceData.length)
+                            : new byte[0];
+            int adjustedRssi = getAdjustedRssi(scanResult);
+            // Default value, used if we have not seen any previous scans.
+            int previousScanEpochSeconds = 0;
+            // Opportunistic scans can only measure time since last stopScanning(), not from each other.
+            if (scanStatusManager.isOpportunisticScanning()) {
+                scanTimePersistence.markLastOpportunisticSightingTime();
+                previousScanEpochSeconds = scanTimePersistence.getLastScanStopTime();
+            } else {
+                // Scheduled sightings measure time since last stopScanning() or since the last
+                // opportunistic sighting, whichever is later.
+                previousScanEpochSeconds =
+                        Math.max(
+                                scanTimePersistence.getLastScanStopTime(),
+                                scanTimePersistence.getLastOpportunisticSightingTime());
+            }
+            Log.log
+                    .atInfo()
+                    // LINT.IfChange
+                    .log(
+                            "Scan device %s, type=%d, id=%s, raw_rssi=%d, calibrated_rssi=%d, meta=%s,"
+                                    + " previous_scan=%d",
+                            scanResult.getDevice(),
+                            callbackType,
+                            id,
+                            scanResult.getRssi(),
+                            adjustedRssi,
+                            metadata.length <= 0 ? "Empty" : Constants.fromIdByteArrayToString(metadata),
+                            previousScanEpochSeconds);
+            // ID is used only for deduping.
+            rescheduler.run();
+            databaseWriter.writeBleSighting(idByte, adjustedRssi, metadata, previousScanEpochSeconds);
+        }
+
+        @Override
+        public void onScanFailed(int errorCode) {
+            Log.log.atWarning().log("Scan failed! errorCode=%d", errorCode);
+            if (errorCode == SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) {
+                startScanFailCallback.run();
+            }
+        }
+
+        private static int getAdjustedRssi(ScanResult scanResult) {
+            int adjustedRssi = scanResult.getRssi() + (int) ContactTracingFeature.rssiOffset();
+            if (adjustedRssi > MAX_RSSI_VALUE) {
+                adjustedRssi = MAX_RSSI_VALUE;
+            } else if (adjustedRssi < MIN_RSSI_VALUE) {
+                adjustedRssi = MIN_RSSI_VALUE;
+            }
+            return adjustedRssi;
+        }
+    }
+
+    @VisibleForTesting
+    static class ScanStatusManager {
+        enum Status {
+            SCANNING,
+            OPPORTUNISTIC_SCANNING,
+            PAUSED,
+            USER_STOPPED_SCANNING
+        }
+
+        private Status status = Status.USER_STOPPED_SCANNING;
+
+        public ScanStatusManager() {
+        }
+
+        public Status getStatus() {
+            return status;
+        }
+
+        public boolean isStopped() {
+            return status == Status.USER_STOPPED_SCANNING;
+        }
+
+        public boolean isScanning() {
+            return status == Status.SCANNING;
+        }
+
+        public boolean isOpportunisticScanning() {
+            return status == Status.OPPORTUNISTIC_SCANNING;
+        }
+
+        public boolean isPaused() {
+            return status == Status.PAUSED;
+        }
+
+        public void setScanning() {
+            setStatus(Status.SCANNING);
+        }
+
+        public void setPaused() {
+            setStatus(Status.PAUSED);
+        }
+
+        public void setOpportunisticScanning() {
+            setStatus(Status.OPPORTUNISTIC_SCANNING);
+        }
+
+        public void setStopped() {
+            setStatus(Status.USER_STOPPED_SCANNING);
+        }
+
+        private void setStatus(Status status) {
+            this.status = status;
+        }
+    }
+
+    private class ScanTimePersistence {
+        private int lastScanStopTimeSeconds = 0;
+        private int lastOpportunisticSightingTimeSeconds = 0;
+
+        public ScanTimePersistence() {
+        }
+
+        public void markLastScanStopTime() {
+            lastScanStopTimeSeconds = (int) MILLISECONDS.toSeconds(currentTimeMsSupplier.get());
+        }
+
+        public void markLastOpportunisticSightingTime() {
+            lastOpportunisticSightingTimeSeconds =
+                    (int) MILLISECONDS.toSeconds(currentTimeMsSupplier.get());
+        }
+
+        public int getLastScanStopTime() {
+            return lastScanStopTimeSeconds;
+        }
+
+        public int getLastOpportunisticSightingTime() {
+            return lastOpportunisticSightingTimeSeconds;
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/utils/BluetoothUtils.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/utils/BluetoothUtils.java
new file mode 100644
index 0000000..499238e
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/utils/BluetoothUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.utils;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+
+import androidx.core.content.ContextCompat;
+
+import java.util.List;
+
+/**
+ * Utility class for bluetooth.
+ */
+@SuppressWarnings("MissingPermission")
+public class BluetoothUtils {
+    public static boolean isBluetoothProfileInUsing(List<Integer> profiles) {
+        /**
+         * FIXME: Get the device's Bluetooth Adapter from
+         * {@link ContextCompat#getSystemService(Context, Class)} with the class
+         * {@link BluetoothManager}
+         */
+        BluetoothAdapter adapter = null;
+        if (adapter == null || !adapter.isEnabled()) {
+            return false;
+        }
+
+        for (int profile : profiles) {
+            int connectingState = adapter.getProfileConnectionState(profile);
+            if (connectingState == BluetoothAdapter.STATE_CONNECTED
+                    || connectingState == BluetoothAdapter.STATE_CONNECTING) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private BluetoothUtils() {
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/utils/Constants.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/utils/Constants.java
new file mode 100644
index 0000000..50c4c78
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/utils/Constants.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.ble.utils;
+
+import static com.google.common.io.BaseEncoding.base16;
+
+import android.os.ParcelUuid;
+
+import com.google.samples.exposurenotification.nearby.ExposureNotificationStatusCodes;
+
+import java.util.UUID;
+
+/** Constants for BLE scanner and advertiser. */
+public class Constants {
+    public static final int METADATA_LENGTH = 4;
+    public static final ParcelUuid CONTACT_TRACER_UUID = new ParcelUuid(to128BitUuid((short) 0xFD6F));
+    private static final int BIT_INDEX_OF_16_BIT_UUID = 32;
+
+    public static UUID to128BitUuid(short shortUuid) {
+        UUID baseUuid = UUID.fromString("00000000-0000-1000-8000-00805F9B34FB");
+        return new UUID(
+                ((shortUuid & 0xFFFFL) << BIT_INDEX_OF_16_BIT_UUID) | baseUuid.getMostSignificantBits(),
+                baseUuid.getLeastSignificantBits());
+    }
+
+    public static String fromIdByteArrayToString(byte[] idByteArray) {
+        return base16().encode(idByteArray);
+    }
+
+    /** The result of start or stop scanning & advertising. */
+    public enum StatusCode {
+        UNKNOWN(ExposureNotificationStatusCodes.FAILED),
+        OK(ExposureNotificationStatusCodes.SUCCESS),
+        ERROR_BLUETOOTH_NOT_SUPPORTED(ExposureNotificationStatusCodes.FAILED_NOT_SUPPORTED),
+        ERROR_BLUETOOTH_DISABLED(ExposureNotificationStatusCodes.FAILED_BLUETOOTH_DISABLED),
+        ERROR_BLUETOOTH_UNKNOWN(ExposureNotificationStatusCodes.INTERNAL_ERROR);
+
+        private final int apiStatusCode;
+
+        StatusCode(int apiStatusCode) {
+            this.apiStatusCode = apiStatusCode;
+        }
+
+        public int getApiStatusCode() {
+            return apiStatusCode;
+        }
+    }
+
+    private Constants() {}
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/AesCtrEncryptor.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/AesCtrEncryptor.java
new file mode 100644
index 0000000..862165c
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/AesCtrEncryptor.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.crypto;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * AesCtrEncryptor helper functions.
+ */
+public class AesCtrEncryptor {
+    private AesCtrEncryptor() {
+    }
+
+    public static byte[] aesCtr(byte[] key, byte[] iv, byte[] data) throws CryptoException {
+        try {
+            Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
+            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
+
+            return cipher.doFinal(data);
+        } catch (NoSuchPaddingException
+                | NoSuchAlgorithmException
+                | InvalidAlgorithmParameterException
+                | InvalidKeyException
+                | BadPaddingException
+                | IllegalBlockSizeException e) {
+            throw new CryptoException(e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/AesEcbEncryptor.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/AesEcbEncryptor.java
new file mode 100644
index 0000000..6dcc175
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/AesEcbEncryptor.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.crypto;
+
+import android.annotation.SuppressLint;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Encryptor that encapsultes aes/ecb encryption, but separates initialization and encryption steps.
+ *
+ * <p>If used repeatedly, call {@link #create()} and {@link #init(byte[])} sparingly.
+ */
+public final class AesEcbEncryptor {
+
+    private final Cipher cipher;
+
+    @SuppressLint("GetInstance")
+    public static AesEcbEncryptor create() throws CryptoException {
+        try {
+            return new AesEcbEncryptor(Cipher.getInstance("AES/ECB/NoPadding"));
+        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+            throw new CryptoException(e);
+        }
+    }
+
+    private AesEcbEncryptor(Cipher cipher) {
+        this.cipher = cipher;
+    }
+
+    /**
+     * Initializes encryption with provided key.
+     */
+    public void init(byte[] key) throws CryptoException {
+        try {
+            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
+        } catch (InvalidKeyException e) {
+            throw new CryptoException(e);
+        }
+    }
+
+    /**
+     * Encrypts provided data.
+     */
+    public byte[] encrypt(byte[] data) throws CryptoException {
+        try {
+            return cipher.doFinal(data);
+        } catch (BadPaddingException | IllegalBlockSizeException e) {
+            throw new CryptoException(e);
+        }
+    }
+
+    /**
+     * Encrypts provided data to a given output buffer.
+     */
+    public byte[] encrypt(byte[] data, byte[] output) throws CryptoException {
+        try {
+            cipher.doFinal(data, /*inputOffset =*/ 0, data.length, output, /* outputOffset =*/ 0);
+            return output;
+        } catch (BadPaddingException | IllegalBlockSizeException | ShortBufferException e) {
+            throw new CryptoException(e);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/CryptoException.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/CryptoException.java
new file mode 100644
index 0000000..5ca7e3f
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/CryptoException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.crypto;
+
+/**
+ * Exception related to cryptographic operations in exposure notification.
+ */
+@SuppressWarnings("unused")
+public class CryptoException extends Exception {
+    public CryptoException() {
+        super();
+    }
+
+    public CryptoException(Throwable throwable) {
+        super(throwable);
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/KeyDerivation.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/KeyDerivation.java
new file mode 100644
index 0000000..91e24b0
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/crypto/KeyDerivation.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.crypto;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Preconditions;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Key derivation helper functions.
+ */
+public final class KeyDerivation {
+    private static final int HKDF_OUTPUT_LENGTH = 16;
+    private static final int HASH_LENGTH = 32;
+    /**
+     * The algorithm name used for Exposure Notification Cryptography.
+     */
+    public static final String ALGORITHM_NAME = "HmacSHA256";
+
+    private KeyDerivation() {
+    }
+
+    /**
+     * Note that this function is for Exposure Notification Cryptography Specification 1.1 only, it
+     * only support 16-byte length output.
+     */
+    public static byte[] hkdfSha256(
+            byte[] inputKeyingMaterial, @Nullable byte[] inputSalt, byte[] info, int length)
+            throws CryptoException {
+        try {
+            Mac mac = Mac.getInstance(ALGORITHM_NAME);
+            return hkdfSha256(mac, inputKeyingMaterial, inputSalt, info, length);
+        } catch (NoSuchAlgorithmException e) {
+            throw new CryptoException(e);
+        }
+    }
+
+    /**
+     * Note that this function is for Exposure Notification Cryptography Specification 1.1 only, it
+     * only support 16-byte length output.
+     */
+    public static byte[] hkdfSha256(
+            Mac mac, byte[] inputKeyingMaterial, @Nullable byte[] inputSalt, byte[] info, int length)
+            throws CryptoException {
+        Preconditions.checkArgument(mac.getAlgorithm().equals(ALGORITHM_NAME));
+        if (length != HKDF_OUTPUT_LENGTH) {
+            throw new CryptoException(new NoSuchAlgorithmException("Only support 16-byte."));
+        }
+        byte[] salt = (inputSalt == null || inputSalt.length == 0) ? new byte[HASH_LENGTH] : inputSalt;
+        try {
+            return hkdfSha256ExpandLength16(mac, hkdfSha256Extract(mac, inputKeyingMaterial, salt), info);
+        } catch (InvalidKeyException e) {
+            throw new CryptoException(e);
+        }
+    }
+
+    /**
+     * The HKDF (RFC 5869) extraction function, using the SHA-256 hash function. The output PRK is
+     * calculated as follows: PRK = HMAC-SHA256(salt, IKM), i.e. salt as the key of hmac-sha256, and
+     * ikm (input keying material) as the message.
+     */
+    private static byte[] hkdfSha256Extract(Mac mac, byte[] inputKeyingMaterial, byte[] salt)
+            throws InvalidKeyException {
+        mac.init(new SecretKeySpec(salt, ALGORITHM_NAME));
+
+        return mac.doFinal(inputKeyingMaterial);
+    }
+
+    /**
+     * HKDF (RFC 5869) expansion function, using the SHA-256 hash function, the output size is
+     * 16-byte.
+     */
+    private static byte[] hkdfSha256ExpandLength16(Mac mac, byte[] pseudoRandomKey, byte[] info)
+            throws InvalidKeyException {
+        // For length being 16 cases, counter always equals to 0x01.
+        byte[] counter = {0x01};
+        mac.init(new SecretKeySpec(pseudoRandomKey, ALGORITHM_NAME));
+        mac.update(info);
+
+        return Arrays.copyOf(mac.doFinal(counter), HKDF_OUTPUT_LENGTH);
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/AssociatedEncryptedMetadata.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/AssociatedEncryptedMetadata.java
new file mode 100644
index 0000000..2b067c8
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/AssociatedEncryptedMetadata.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data;
+
+import com.google.samples.ByteArrayValue;
+
+/**
+ * The Associated Encrypted Metadata is data encrypted along with the Rolling Proximity Identifier
+ * and that can only be decrypted later, if the user broadcasting it is tested positive and reveals
+ * their Temporary Exposure Key.
+ */
+public final class AssociatedEncryptedMetadata extends ByteArrayValue {
+
+    /**
+     * Create an {@link AssociatedEncryptedMetadata} by taking ownership of a {@code byte[]}.
+     *
+     * <p>WARNING: This value could be unexpectedly mutated if {@code data} will still be accessible
+     * to the caller. To ensure an immutable value, use {@code new AssociatedEncryptedMetadata(data,
+     * false)} to construct from a copy of {@code data}.
+     */
+    public static AssociatedEncryptedMetadata create(byte[] data) {
+        return new AssociatedEncryptedMetadata(data, /*takeOwnership=*/ true);
+    }
+
+    public AssociatedEncryptedMetadata(byte[] data, boolean takeOwnership) {
+        super(data, takeOwnership);
+    }
+
+    /**
+     * Return a direct reference to the underlying data.
+     *
+     * <p>WARNING: This breaks the encapsulation and could allow the value to be mutated. Only use in
+     * performance sensitive cases where the cost of cloning is too high, and the use of the {@code
+     * byte[]} is guaranteed to be read-only.
+     */
+    public byte[] getDirect() {
+        return exposeInternalBytesAndRiskMutation();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/BluetoothMetadata.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/BluetoothMetadata.java
new file mode 100644
index 0000000..8dd77e9
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/BluetoothMetadata.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data;
+
+import com.google.auto.value.AutoValue;
+import com.google.samples.exposurenotification.ExposureNotificationEnums.CalibrationConfidence;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * The structured metadata sent and received via Bluetooth protocol. The actual payload that get
+ * transmitted over Bluetooth is an encrypted copy of this class. See {@link
+ * AssociatedEncryptedMetadata} and {@link
+ * com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataGenerator}.
+ */
+@AutoValue
+public abstract class BluetoothMetadata {
+    public static final byte VERSION_1_0 = 0x40;
+    public static final byte VERSION_1_1 = 0x50;
+
+    public static final int CALIBRATION_CONFIDENCE_BITS_OFFSET = 2;
+    public static final byte CALIBRATION_CONFIDENCE_BITS_MASK = (byte) 0b00001100;
+
+    public static BluetoothMetadata create(byte version, byte txPower) {
+        return new AutoValue_BluetoothMetadata(version, txPower);
+    }
+
+    // Bits 7:6 = major version
+    // Bits 5:4 = minor version
+    // Bits 3:2 = transmit power calibration confidence (added in v1.1)
+    // Bits 1:0 = reserved
+    public abstract byte version();
+
+    public abstract byte txPower();
+
+    public byte[] toBytes() {
+        // Byte 0 = version
+        // Byte 1 = TX power
+        // Byte 2/3 = reserved
+        return new byte[]{version(), txPower(), 0, 0};
+    }
+
+    public CalibrationConfidence calibrationConfidence() {
+        CalibrationConfidence confidence =
+                CalibrationConfidence.forNumber(
+                        (version() & CALIBRATION_CONFIDENCE_BITS_MASK) >> CALIBRATION_CONFIDENCE_BITS_OFFSET);
+        if (confidence == null) {
+            confidence = CalibrationConfidence.LOWEST_CONFIDENCE;
+        }
+        return confidence;
+    }
+
+    /**
+     * Parse the {@link BluetoothMetadata} from a raw byte array.
+     *
+     * @param rawMetadata the decrypted raw metadata from {@link AssociatedEncryptedMetadata}.
+     */
+    public static BluetoothMetadata fromBytes(byte[] rawMetadata) {
+        checkArgument(rawMetadata.length == 4);
+        return BluetoothMetadata.create(/*version=*/ rawMetadata[0], /*txPower=*/ rawMetadata[1]);
+    }
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/DayNumber.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/DayNumber.java
new file mode 100644
index 0000000..f53a926
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/DayNumber.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data;
+
+import org.joda.time.Instant;
+
+import java.nio.ByteBuffer;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.concurrent.TimeUnit.DAYS;
+
+/**
+ * A day number is also known as exposure key rolling period number or EKRollingPeriod number. We
+ * call it day number internally because the exposure key rolling period is 144 * 10 minutes or 24
+ * hours. Each day number represents a 24-hour window that is based on Unix Epoch Time and is
+ * timezone independent.
+ *
+ * <p>The value shall be in [0, 65535] interval. Though 18366 is the smallest legitimate day number
+ * as of 2020/04/14, we allow the day number to be smaller than that to properly handle wrong system
+ * clock on some devices.
+ */
+public class DayNumber {
+    public static final DayNumber MIN_DAY_NUMBER = new DayNumber(0);
+    public static final DayNumber MAX_DAY_NUMBER = new DayNumber(0xFFFF);
+    private static final int SIZE_BYTES = 2;
+
+    private final int value;
+
+    public DayNumber(Instant instant) {
+        value = getDayNumber(instant);
+    }
+
+    public DayNumber(int value) {
+        this.value = value;
+    }
+
+    public int getValue() {
+        return value;
+    }
+
+    /**
+     * Puts the 2 bytes (big-endian) representation of this {@link DayNumber} in a {@link ByteBuffer}.
+     * The 2 bytes can represent a day number till 2149/06/07.
+     *
+     * @param byteBuffer the destination buffer to {@link ByteBuffer#put} the byte representation.
+     */
+    public void putIn(ByteBuffer byteBuffer) {
+        putIn(value, byteBuffer);
+    }
+
+    /**
+     * The static version of {@link #putIn(ByteBuffer)}.
+     */
+    public static void putIn(int dayNumber, ByteBuffer byteBuffer) {
+        checkArgument(byteBuffer.remaining() >= SIZE_BYTES);
+        byteBuffer.putShort((short) dayNumber);
+    }
+
+    /**
+     * Gets a {@link DayNumber} from the 2 bytes (big-endian) representation in a {@link ByteBuffer}.
+     */
+    public static DayNumber getFrom(ByteBuffer byteBuffer) {
+        return new DayNumber(getValueFrom(byteBuffer));
+    }
+
+    /**
+     * The static version of {@link #getFrom(ByteBuffer)}.
+     */
+    public static int getValueFrom(ByteBuffer byteBuffer) {
+        checkArgument(byteBuffer.remaining() >= SIZE_BYTES);
+        return ((int) byteBuffer.getShort()) & 0xFFFF; // Equivalent of Java 8 Short.toUnsignedInt().
+    }
+
+    /**
+     * Returns the int representation of a day number of an {@link Instant}.
+     *
+     * @param instant the instant to get day number of
+     * @return day number with the range in [0, 65535] interval, or undefined if the {@code instant}
+     * is later than June 6, 2149 12:00:00 AM GMT.
+     */
+    public static int getDayNumber(Instant instant) {
+        return (int) (instant.getMillis() / DAYS.toMillis(1));
+    }
+
+    public static int getSizeBytes() {
+        return SIZE_BYTES;
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/GeneratedRollingProximityId.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/GeneratedRollingProximityId.java
new file mode 100644
index 0000000..72cc614
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/GeneratedRollingProximityId.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * A {@link RollingProximityId} with a concept of what time interval number it was broadcasted for.
+ *
+ * <p>This class is intentionally different from {@link RollingProximityId} in that this class is
+ * owned by system internally while {@link RollingProximityId}s are ids observed externally by
+ * scanning.
+ */
+@AutoValue
+public abstract class GeneratedRollingProximityId {
+
+    /**
+     * The {@link RollingProximityId} value.
+     */
+    public abstract RollingProximityId rollingProximityId();
+
+    /**
+     * The associated interval number that is encrypted inside of the {@link RollingProximityId}.
+     */
+    public abstract int intervalNumber();
+
+    public static GeneratedRollingProximityId create(
+            RollingProximityId rollingProximityId, int intervalNumber) {
+        return new AutoValue_GeneratedRollingProximityId(rollingProximityId, intervalNumber);
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/RollingProximityId.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/RollingProximityId.java
new file mode 100644
index 0000000..138301b
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/RollingProximityId.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data;
+
+import com.google.samples.ByteArrayValue;
+
+/**
+ * Represents a Rolling Proximity Identifier.
+ */
+@SuppressWarnings("unused")
+public final class RollingProximityId extends ByteArrayValue {
+
+    private static final byte[] MIN_VALUE = {
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+    };
+    private static final byte[] MAX_VALUE = {
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF
+    };
+    public static final RollingProximityId MIN_ID = new RollingProximityId(MIN_VALUE);
+    public static final RollingProximityId MAX_ID = new RollingProximityId(MAX_VALUE);
+
+    public RollingProximityId(byte[] value) {
+        super(value);
+    }
+
+    /**
+     * Constructor which will take ownership of {@code value} if {@code takeOwnership} is true.
+     *
+     * <p>WARNING: This constructor can leave this {@link RollingProximityId} open to unexpected
+     * mutation. Use only in cases where the {@code byte[]} is a temporary, to avoid the overhead of
+     * cloning an array which will immediately go out of scope.
+     */
+    public RollingProximityId(byte[] value, boolean takeOwnership) {
+        super(value, takeOwnership);
+    }
+
+    /**
+     * Return a direct reference to the underlying data.
+     *
+     * <p>WARNING: This breaks the encapsulation and could allow the value to be mutated. Only use in
+     * performance sensitive cases where the cost of cloning is too high, and the use of the {@code
+     * byte[]} is guaranteed to be read-only.
+     */
+    public byte[] getDirect() {
+        return exposeInternalBytesAndRiskMutation();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/TemporaryExposureKeySupport.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/TemporaryExposureKeySupport.java
new file mode 100644
index 0000000..76d374d
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/TemporaryExposureKeySupport.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data;
+
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+/**
+ * A utility class providing helper functions for {@link TemporaryExposureKey}.
+ */
+public final class TemporaryExposureKeySupport {
+    private static final byte[] MIN_VALUE = {
+            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+    };
+    private static final byte[] MAX_VALUE = {
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF,
+            (byte) 0xFF
+    };
+
+    private TemporaryExposureKeySupport() {
+    }
+
+    public static DayNumber getDayNumber(TemporaryExposureKey temporaryExposureKey) {
+        return new DayNumber(
+                (int)
+                        (temporaryExposureKey.getRollingStartIntervalNumber()
+                                / ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod()));
+    }
+
+    /**
+     * Gets the time which a key is released from embargo. This time is the key's expiration date (ie,
+     * start period + number of rolling periods valid for) + an additional number of rolling periods
+     * to protect against clock drift.
+     */
+    public static long getEmbargoEndTimeInMillis(TemporaryExposureKey temporaryExposureKey) {
+        long endIntervalNumber =
+                temporaryExposureKey.getRollingStartIntervalNumber()
+                        + temporaryExposureKey.getRollingPeriod()
+                        + ContactTracingFeature.tkMatchingClockDriftRollingPeriods();
+        return endIntervalNumber * MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes());
+    }
+
+    public static int getRollingStartIntervalNumber(DayNumber dayNumber) {
+        return getRollingStartIntervalNumber(dayNumber.getValue());
+    }
+
+    public static int getRollingStartIntervalNumber(int dayNumber) {
+        return (int) (dayNumber * ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
+    }
+
+    /**
+     * Get the rolling end interval number (exclusive). In another word, this number is the first
+     * interval number that does not belong to this {@link TemporaryExposureKey} following the {@link
+     * TemporaryExposureKey#getRollingStartIntervalNumber()}.
+     */
+    public static int getRollingEndIntervalNumber(TemporaryExposureKey temporaryExposureKey) {
+        return temporaryExposureKey.getRollingStartIntervalNumber()
+                + temporaryExposureKey.getRollingPeriod();
+    }
+
+    /**
+     * Get the maximum possible rolling end interval number (exclusive). In another word, this number
+     * is the first interval number that does not belong to this {@link TemporaryExposureKey}
+     * following the {@link TemporaryExposureKey#getRollingStartIntervalNumber()} under assumption of
+     * maximum key validity.
+     */
+    public static int getMaxPossibleRollingEndIntervalNumber(
+            TemporaryExposureKey temporaryExposureKey) {
+        return (int)
+                (temporaryExposureKey.getRollingStartIntervalNumber()
+                        + ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
+    }
+
+    /**
+     * Get the end time of the validation window of {@code temporaryExposureKey} without considering
+     * embargo. See {@link #getEmbargoEndTimeInMillis(TemporaryExposureKey)} for embargo end time.
+     */
+    public static long getExpirationTimeMillis(TemporaryExposureKey temporaryExposureKey) {
+        return getRollingEndIntervalNumber(temporaryExposureKey)
+                * MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes());
+    }
+
+    public static int getKeySizeBytes() {
+        return (int) ContactTracingFeature.temporaryTracingKeySizeBytes();
+    }
+
+    public static TemporaryExposureKey getMinKey(DayNumber dayNumber) {
+        return new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                .setRollingStartIntervalNumber(getRollingStartIntervalNumber(dayNumber))
+                .setKeyData(MIN_VALUE)
+                .build();
+    }
+
+    public static TemporaryExposureKey getMaxKey(DayNumber dayNumber) {
+        return new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                .setRollingStartIntervalNumber(getRollingStartIntervalNumber(dayNumber))
+                .setKeyData(MAX_VALUE)
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/fileformat/StreamingTemporaryExposureKeyFileParser.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/fileformat/StreamingTemporaryExposureKeyFileParser.java
new file mode 100644
index 0000000..bcf135d
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/fileformat/StreamingTemporaryExposureKeyFileParser.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data.fileformat;
+
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.WireFormat;
+import com.google.samples.exposurenotification.ExposureKeyExportProto;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKeyExport;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.matching.TemporaryExposureKeyExportV1Header;
+import com.google.samples.exposurenotification.storage.CloseableIterable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * A class reads and parses a temporary exposure key export file in a streaming mode meaning that
+ * the memory usage is independent to the file size.
+ *
+ * <p>See https://developers.google.com/android/exposure-notifications/exposure-key-file-format
+ */
+public class StreamingTemporaryExposureKeyFileParser {
+
+    /**
+     * Reads and parses the given {@link File} as {@link CloseableIterable<TemporaryExposureKey>}.
+     *
+     * <p>Parsing different files concurrently is safe. Parsing the same file before the previous
+     * returned {@link CloseableIterable} closes is not allowed.
+     *
+     * @param file The key export file
+     * @return A single-use iterable that must be closed after use. Not thread-safe.
+     */
+    public static CloseableIterable<TemporaryExposureKey> parse(File file) throws IOException {
+        return ParserIterable.create(new FileInputStream(file));
+    }
+
+    /** Same as {@link #parse(File)} but takes an {@link InputStream} instead. */
+    public static CloseableIterable<TemporaryExposureKey> parse(InputStream inputStream)
+            throws IOException {
+        return ParserIterable.create(inputStream);
+    }
+
+    /** The {@link CloseableIterable} that handles file reading while iteration. */
+    private static class ParserIterable implements CloseableIterable<TemporaryExposureKey> {
+        private final InputStream inputStream;
+        private final CodedInputStream codedStream;
+
+        // The tag (that has been read) for the field that yet to be read next. At anywhere outside of
+        // readUntilNextKeyTagOrEnd(), the value of nextTag can only be KEYS_FIELD_NUMBER or 0 because
+        // we always skip all other fields.
+        private int nextTag;
+
+        public static ParserIterable create(InputStream inputStream) throws IOException {
+            ParserIterable parser = new ParserIterable(inputStream);
+
+            // Prime the parser. Do this from outside of the constructor, to keep the nullness
+            // checker happy.
+            TemporaryExposureKeyExportV1Header.readAndVerifyHeader(inputStream);
+            parser.readUntilNextKeyTagOrEnd();
+
+            return parser;
+        }
+
+        private ParserIterable(InputStream inputStream) {
+            this.inputStream = inputStream;
+            codedStream = CodedInputStream.newInstance(inputStream);
+        }
+
+        @Override
+        public void close() throws IOException {
+            inputStream.close();
+        }
+
+        @Override
+        public Iterator<TemporaryExposureKey> iterator() {
+            return new Iterator<TemporaryExposureKey>() {
+                @Override
+                public boolean hasNext() {
+                    return nextTag != 0;
+                }
+
+                @Override
+                public TemporaryExposureKey next() {
+                    return readNextKey();
+                }
+            };
+        }
+
+        private void readUntilNextKeyTagOrEnd() throws IOException {
+            nextTag = codedStream.readTag();
+            while (nextTag != 0 && !isTagForKeys(nextTag)) {
+                codedStream.skipField(nextTag);
+                nextTag = codedStream.readTag();
+            }
+        }
+
+        private TemporaryExposureKey readNextKey() {
+            if (!isTagForKeys(nextTag)) {
+                throw new NoSuchElementException("Unexpected proto buffer field");
+            }
+
+            try {
+                ExposureKeyExportProto.TemporaryExposureKey.Builder builder =
+                        ExposureKeyExportProto.TemporaryExposureKey.newBuilder();
+                codedStream.readMessage(builder, ExtensionRegistryLite.getEmptyRegistry());
+                readUntilNextKeyTagOrEnd();
+                return new TemporaryExposureKeyConverter().convert(builder.build());
+            } catch (IOException e) {
+                throw new NoSuchElementException("IOException: " + e.getMessage());
+            }
+        }
+    }
+
+    private static boolean isTagForKeys(int tag) {
+        return WireFormat.getTagFieldNumber(tag) == TemporaryExposureKeyExport.KEYS_FIELD_NUMBER;
+    }
+
+    private StreamingTemporaryExposureKeyFileParser() {}
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/fileformat/TemporaryExposureKeyConverter.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/fileformat/TemporaryExposureKeyConverter.java
new file mode 100644
index 0000000..393dea7
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/fileformat/TemporaryExposureKeyConverter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data.fileformat;
+
+
+import com.google.common.base.Converter;
+import com.google.samples.exposurenotification.ExposureKeyExportProto;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+
+/**
+ * Converts temporary exposure key from the export proto format to Android parsable format.
+ */
+public class TemporaryExposureKeyConverter
+        extends Converter<ExposureKeyExportProto.TemporaryExposureKey, TemporaryExposureKey> {
+
+    @Override
+    protected TemporaryExposureKey doForward(ExposureKeyExportProto.TemporaryExposureKey exportKey) {
+        return new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                .setKeyData(exportKey.getKeyData().toByteArray())
+                .setRollingStartIntervalNumber(exportKey.getRollingStartIntervalNumber())
+                .setRollingPeriod(exportKey.getRollingPeriod())
+                .setTransmissionRiskLevel(exportKey.getTransmissionRiskLevel())
+                .build();
+    }
+
+    @Override
+    protected ExposureKeyExportProto.TemporaryExposureKey doBackward(
+            TemporaryExposureKey temporaryExposureKey) {
+        throw new UnsupportedOperationException(
+                "Converting from Android parceable to export proto is not supported");
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataGenerator.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataGenerator.java
new file mode 100644
index 0000000..6210cac
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataGenerator.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data.generator;
+
+import android.os.Build.VERSION_CODES;
+
+import androidx.annotation.RequiresApi;
+
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.crypto.CryptoException;
+import com.google.samples.exposurenotification.crypto.KeyDerivation;
+import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
+import com.google.samples.exposurenotification.data.BluetoothMetadata;
+import com.google.samples.exposurenotification.data.RollingProximityId;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+
+/**
+ * The class to encrypt (from {@link BluetoothMetadata} to {@link AssociatedEncryptedMetadata}) and
+ * decrypt (opposite) metadata.
+ */
+@RequiresApi(api = VERSION_CODES.KITKAT)
+public class AssociatedEncryptedMetadataGenerator {
+    private final byte[] aemKey;
+
+    /**
+     * Construct a {@link AssociatedEncryptedMetadataGenerator} with a {@link TemporaryExposureKey}
+     * which is required to encrypt and decrypt metadata.
+     *
+     * @deprecated Use {@link Factory} to create {@link AssociatedEncryptedMetadataGenerator}
+     * instances.
+     */
+    @Deprecated
+    public AssociatedEncryptedMetadataGenerator(TemporaryExposureKey temporaryExposureKey)
+            throws CryptoException {
+        aemKey =
+                AssociatedEncryptedMetadataHelper.generateAemKey(
+                        temporaryExposureKey.getKeyData(),
+                        ContactTracingFeature.aemkHkdfInfoString(),
+                        (int) ContactTracingFeature.associatedMetadataEncryptionKeySizeBytes());
+    }
+
+    /**
+     * Construct a {@link AssociatedEncryptedMetadataGenerator} with a {@link TemporaryExposureKey}
+     * which is required to encrypt and decrypt metadata.
+     */
+    private AssociatedEncryptedMetadataGenerator(
+            Mac mac, TemporaryExposureKey temporaryExposureKey, byte[] aemkHkdfInfoBytes)
+            throws CryptoException {
+        aemKey =
+                AssociatedEncryptedMetadataHelper.generateAemKey(
+                        mac,
+                        temporaryExposureKey.getKeyData(),
+                        aemkHkdfInfoBytes,
+                        (int) ContactTracingFeature.associatedMetadataEncryptionKeySizeBytes());
+    }
+
+    /**
+     * Encrypts BLE metadata using AES-CTR(AEMK, RPI, metadata).
+     */
+    public AssociatedEncryptedMetadata encrypt(
+            RollingProximityId rollingProximityId, BluetoothMetadata metadata) throws CryptoException {
+        return AssociatedEncryptedMetadataHelper.encrypt(rollingProximityId.get(), metadata, aemKey);
+    }
+
+    /**
+     * Decrypts using AES-CTR(AEMK, RPI, encryptedData).
+     */
+    public BluetoothMetadata decrypt(
+            RollingProximityId rollingProximityId, AssociatedEncryptedMetadata aem)
+            throws CryptoException {
+        return BluetoothMetadata.fromBytes(
+                AssociatedEncryptedMetadataHelper.encryptOrDecrypt(
+                        aemKey, rollingProximityId.getDirect(), aem.getDirect()));
+    }
+
+    /**
+     * Factory to create {@link AssociatedEncryptedMetadataGenerator}.
+     */
+    public static class Factory {
+        private final Mac mac;
+
+        public Factory() throws CryptoException {
+            try {
+                mac = Mac.getInstance(KeyDerivation.ALGORITHM_NAME);
+            } catch (NoSuchAlgorithmException e) {
+                throw new CryptoException(e);
+            }
+        }
+
+        /**
+         * Gets an instance of {@link AssociatedEncryptedMetadataGenerator}.
+         */
+        public AssociatedEncryptedMetadataGenerator getInstance(
+                TemporaryExposureKey temporaryExposureKey) throws CryptoException {
+            return new AssociatedEncryptedMetadataGenerator(
+                    mac,
+                    temporaryExposureKey,
+                    ContactTracingFeature.aemkHkdfInfoString().getBytes(StandardCharsets.UTF_8));
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataHelper.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataHelper.java
new file mode 100644
index 0000000..96b0044
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataHelper.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data.generator;
+
+import com.google.samples.exposurenotification.crypto.AesCtrEncryptor;
+import com.google.samples.exposurenotification.crypto.CryptoException;
+import com.google.samples.exposurenotification.crypto.KeyDerivation;
+import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
+import com.google.samples.exposurenotification.data.BluetoothMetadata;
+
+import javax.crypto.Mac;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Helper class for the {@link AssociatedEncryptedMetadataGenerator} .
+ */
+public class AssociatedEncryptedMetadataHelper {
+
+    /**
+     * Generates the Associated Encryption Metadata key by the given temporary exposure key.
+     *
+     * @deprecated use {@link #generateAemKey(Mac, byte[], byte[], int)} instead.
+     */
+    @Deprecated
+    @SuppressWarnings("NewApi")
+    public static byte[] generateAemKey(
+            byte[] temporaryExposureKey,
+            String aemkHkdfInfoString,
+            int associatedMetadataEncryptionKeySizeBytes)
+            throws CryptoException {
+        // AEMK = HKDF(tek, NULL, UTF8("EN-AEMK"), 16).
+        return KeyDerivation.hkdfSha256(
+                temporaryExposureKey,
+                /* inputSalt =*/ null,
+                aemkHkdfInfoString.getBytes(UTF_8),
+                associatedMetadataEncryptionKeySizeBytes);
+    }
+
+    /**
+     * Generates the Associated Encryption Metadata key by the given temporary exposure key.
+     */
+    @SuppressWarnings("NewApi")
+    public static byte[] generateAemKey(
+            Mac mac,
+            byte[] temporaryExposureKey,
+            byte[] aemkHkdfInfoBytes,
+            int associatedMetadataEncryptionKeySizeBytes)
+            throws CryptoException {
+        // AEMK = HKDF(tek, NULL, UTF8("EN-AEMK"), 16).
+        return KeyDerivation.hkdfSha256(
+                mac,
+                temporaryExposureKey,
+                /* inputSalt =*/ null,
+                aemkHkdfInfoBytes,
+                associatedMetadataEncryptionKeySizeBytes);
+    }
+
+    /**
+     * Encrypt BLE metadata using AES-CTR(AEMK, RPI, metadata).
+     */
+    public static byte[] encryptOrDecrypt(
+            byte[] aemKey, byte[] rollingProximityId, byte[] bluetoothMetadataAsBytes)
+            throws CryptoException {
+        return AesCtrEncryptor.aesCtr(aemKey, rollingProximityId, bluetoothMetadataAsBytes);
+    }
+
+    /**
+     * Create an {@link AssociatedEncryptedMetadata} encrypted via {@code aemKey}.
+     */
+    public static AssociatedEncryptedMetadata encrypt(
+            byte[] rollingProximityIdAsBytes, BluetoothMetadata metadata, byte[] aemKey)
+            throws CryptoException {
+        return AssociatedEncryptedMetadata.create(
+                AssociatedEncryptedMetadataHelper.encryptOrDecrypt(
+                        aemKey, rollingProximityIdAsBytes, metadata.toBytes()));
+    }
+
+    private AssociatedEncryptedMetadataHelper() {
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGenerator.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGenerator.java
new file mode 100644
index 0000000..e491a1f
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGenerator.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data.generator;
+
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.crypto.AesEcbEncryptor;
+import com.google.samples.exposurenotification.crypto.CryptoException;
+import com.google.samples.exposurenotification.crypto.KeyDerivation;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
+
+import javax.crypto.Mac;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+/**
+ * A class to generate {@code RollingProximityId}(s).
+ */
+public class RollingProximityIdGenerator extends RollingProximityIdGeneratorBase {
+
+    /**
+     * Used primarily to avoid re-creating {@link AesEcbEncryptor} for performance.
+     *
+     * @deprecated Use {@link Factory} to create {@link RollingProximityIdGenerator} instances.
+     */
+    @Deprecated
+    public RollingProximityIdGenerator(
+            AesEcbEncryptor aesEcbEncryptor,
+            byte[] keyData,
+            int rollingStartIntervalNumber,
+            int rollingEndIntervalNumber,
+            int rollingProximityIdKeySizeBytes,
+            String rpikHkdfInfoString,
+            String rpidAesPaddedString)
+            throws CryptoException {
+        super(
+                aesEcbEncryptor,
+                keyData,
+                rollingStartIntervalNumber,
+                rollingEndIntervalNumber,
+                rollingProximityIdKeySizeBytes,
+                rpikHkdfInfoString,
+                rpidAesPaddedString);
+    }
+
+    /**
+     * Used primarily to avoid re-creating {@link Mac}, and {@link AesEcbEncryptor} for performance.
+     */
+    private RollingProximityIdGenerator(
+            Mac mac,
+            AesEcbEncryptor aesEcbEncryptor,
+            byte[] keyData,
+            int rollingStartIntervalNumber,
+            int rollingEndIntervalNumber,
+            int rollingProximityIdKeySizeBytes,
+            byte[] rpikHkdfInfoBytes,
+            byte[] rpidAesPaddedBytes,
+            PaddedDataCache paddedDataCache)
+            throws CryptoException {
+        super(
+                mac,
+                aesEcbEncryptor,
+                keyData,
+                rollingStartIntervalNumber,
+                rollingEndIntervalNumber,
+                rollingProximityIdKeySizeBytes,
+                rpikHkdfInfoBytes,
+                rpidAesPaddedBytes,
+                paddedDataCache);
+    }
+
+    /**
+     * @deprecated Use {@link Factory} to create {@link RollingProximityIdGenerator} instances.
+     */
+    @Deprecated
+    public RollingProximityIdGenerator(
+            byte[] keyData,
+            int rollingStartIntervalNumber,
+            int rollingEndIntervalNumber,
+            int rollingProximityIdKeySizeBytes,
+            String rpikHkdfInfoString,
+            String rpidAesPaddedString)
+            throws CryptoException {
+        this(
+                AesEcbEncryptor.create(),
+                keyData,
+                rollingStartIntervalNumber,
+                rollingEndIntervalNumber,
+                rollingProximityIdKeySizeBytes,
+                rpikHkdfInfoString,
+                rpidAesPaddedString);
+    }
+
+    /**
+     * Factory to create {@link RollingProximityIdGenerator}.
+     */
+    public static class Factory {
+        private final Mac mac;
+        private final PaddedDataCache paddedDataCache;
+
+        public Factory() throws CryptoException {
+            int startRollingEnIntervalNumber = getStartRollingEnIntervalNumber(Instant.now());
+            int baseEnIntervalNumber =
+                    startRollingEnIntervalNumber
+                            - (int)
+                            (ContactTracingFeature.contactRecordDaysToKeep()
+                                    * ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
+            paddedDataCache =
+                    PaddedDataCache.createInstance(
+                            baseEnIntervalNumber,
+                            (int) (ContactTracingFeature.contactRecordDaysToKeep() + 1),
+                            ContactTracingFeature.rpidAesPaddedString().getBytes(UTF_8),
+                            (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
+
+            try {
+                mac = Mac.getInstance(KeyDerivation.ALGORITHM_NAME);
+            } catch (NoSuchAlgorithmException e) {
+                throw new CryptoException(e);
+            }
+        }
+
+        /**
+         * Gets an instance of {@link RollingProximityIdGenerator}.
+         */
+        public RollingProximityIdGenerator getInstance(
+                AesEcbEncryptor aesEcbEncryptor,
+                byte[] keyData,
+                int rollingStartIntervalNumber,
+                int rollingEndIntervalNumber,
+                int rollingProximityIdKeySizeBytes,
+                String rpikHkdfInfoString,
+                String rpidAesPaddedString)
+                throws CryptoException {
+            if (rollingEndIntervalNumber - rollingStartIntervalNumber > 144) {
+                Log.log
+                        .atWarning()
+                        .log("RPI generator window is greater than 144. startIntervalNumber=%d,"
+                                        + " endIntervalNumber=%d",
+                                rollingStartIntervalNumber, rollingEndIntervalNumber);
+            }
+            return new RollingProximityIdGenerator(
+                    mac,
+                    aesEcbEncryptor,
+                    keyData,
+                    rollingStartIntervalNumber,
+                    rollingEndIntervalNumber,
+                    rollingProximityIdKeySizeBytes,
+                    rpikHkdfInfoString.getBytes(UTF_8),
+                    rpidAesPaddedString.getBytes(UTF_8),
+                    paddedDataCache);
+        }
+
+        private static int getStartRollingEnIntervalNumber(Instant instant) {
+            int tkRollingPeriod =
+                    (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
+            return tkRollingPeriod * ((int) (instant.toEpochMilli()
+                    / MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes()))
+                    / tkRollingPeriod);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGeneratorBase.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGeneratorBase.java
new file mode 100644
index 0000000..6cd60c1
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/RollingProximityIdGeneratorBase.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data.generator;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+import com.google.samples.exposurenotification.crypto.AesEcbEncryptor;
+import com.google.samples.exposurenotification.crypto.CryptoException;
+import com.google.samples.exposurenotification.crypto.KeyDerivation;
+import com.google.samples.exposurenotification.data.GeneratedRollingProximityId;
+import com.google.samples.exposurenotification.data.RollingProximityId;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * A class to generate {@link RollingProximityId}(s).
+ */
+public class RollingProximityIdGeneratorBase {
+    private static final int AES_BLOCK_SIZE = 16;
+    private final int rollingStartIntervalNumber;
+    private final int rollingEndIntervalNumber;
+    private final AesEcbEncryptor encryptor;
+    private final byte[] aesPadding;
+    @Nullable
+    private PaddedDataCache paddedDataCache;
+
+    /**
+     * Used primarily to avoid re-creating {@link AesEcbEncryptor} for performance.
+     *
+     * @deprecated use {@link #RollingProximityIdGeneratorBase(Mac, AesEcbEncryptor, byte[], int, int,
+     * int, byte[], byte[], PaddedDataCache)} instead.
+     */
+    @Deprecated
+    @SuppressWarnings("NewApi")
+    public RollingProximityIdGeneratorBase(
+            AesEcbEncryptor aesEcbEncryptor,
+            byte[] keyData,
+            int rollingStartIntervalNumber,
+            int rollingEndIntervalNumber,
+            int rollingProximityIdKeySizeBytes,
+            String rpikHkdfInfoString,
+            String rpidAesPaddedString)
+            throws CryptoException {
+        this.rollingStartIntervalNumber = rollingStartIntervalNumber;
+        this.rollingEndIntervalNumber = rollingEndIntervalNumber;
+        aesPadding = rpidAesPaddedString.getBytes(UTF_8);
+        encryptor = aesEcbEncryptor;
+        encryptor.init(generateRpiKey(keyData, rpikHkdfInfoString, rollingProximityIdKeySizeBytes));
+    }
+
+    /**
+     * Used primarily to avoid re-creating {@link Mac}, and {@link AesEcbEncryptor} for performance.
+     */
+    @SuppressWarnings("NewApi")
+    public RollingProximityIdGeneratorBase(
+            Mac mac,
+            AesEcbEncryptor aesEcbEncryptor,
+            byte[] keyData,
+            int rollingStartIntervalNumber,
+            int rollingEndIntervalNumber,
+            int rollingProximityIdKeySizeBytes,
+            byte[] rpikHkdfInfoBytes,
+            byte[] rpidAesPaddedBytes,
+            PaddedDataCache paddedDataCache)
+            throws CryptoException {
+        this.rollingStartIntervalNumber = rollingStartIntervalNumber;
+        this.rollingEndIntervalNumber = rollingEndIntervalNumber;
+        this.paddedDataCache = paddedDataCache;
+        aesPadding = rpidAesPaddedBytes;
+        encryptor = aesEcbEncryptor;
+        encryptor.init(generateRpiKey(mac, keyData, rpikHkdfInfoBytes, rollingProximityIdKeySizeBytes));
+    }
+
+    public RollingProximityIdGeneratorBase(
+            byte[] keyData,
+            int rollingStartIntervalNumber,
+            int rollingEndIntervalNumber,
+            int rollingProximityIdKeySizeBytes,
+            String rpikHkdfInfoString,
+            String rpidAesPaddedString)
+            throws CryptoException {
+        this(
+                AesEcbEncryptor.create(),
+                keyData,
+                rollingStartIntervalNumber,
+                rollingEndIntervalNumber,
+                rollingProximityIdKeySizeBytes,
+                rpikHkdfInfoString,
+                rpidAesPaddedString);
+    }
+
+    /**
+     * Generates a list of rolling proximity ids.
+     */
+    @SuppressWarnings("NewApi")
+    public ImmutableList<GeneratedRollingProximityId> generateIds(byte[] reusedOutput)
+            throws CryptoException {
+        int numIds = rollingEndIntervalNumber - rollingStartIntervalNumber;
+        byte[] paddedData = null;
+        if (paddedDataCache != null && numIds <= paddedDataCache.getIdsPerKey()) {
+            paddedData = paddedDataCache.getCachedData(rollingStartIntervalNumber);
+        }
+        if (paddedData == null) {
+            paddedData = generateAggregatePaddedData(rollingStartIntervalNumber, numIds, aesPadding);
+        }
+        return convertToList(encryptor.encrypt(paddedData, reusedOutput), numIds);
+    }
+
+    /**
+     * Generates the Rolling Proximity Id key by the given temporary exposure key.
+     */
+    @VisibleForTesting
+    @SuppressWarnings("NewApi")
+    static byte[] generateRpiKey(
+            byte[] temporaryExposureKey, String rpikHkdfInfoString, int rollingProximityIdKeySizeBytes)
+            throws CryptoException {
+        // RPIK = HKDF(tek, NULL, UTF8("EN-RPIK"), 16).
+        return KeyDerivation.hkdfSha256(
+                temporaryExposureKey,
+                /* inputSalt =*/ null,
+                rpikHkdfInfoString.getBytes(UTF_8),
+                rollingProximityIdKeySizeBytes);
+    }
+
+    /**
+     * Generates the Rolling Proximity Id key by the given temporary exposure key.
+     */
+    @SuppressWarnings("NewApi")
+    private static byte[] generateRpiKey(
+            Mac mac,
+            byte[] temporaryExposureKey,
+            byte[] rpikHkdfInfoBytes,
+            int rollingProximityIdKeySizeBytes)
+            throws CryptoException {
+        // RPIK = HKDF(tek, NULL, UTF8("EN-RPIK"), 16).
+        return KeyDerivation.hkdfSha256(
+                mac,
+                temporaryExposureKey,
+                /* inputSalt =*/ null,
+                rpikHkdfInfoBytes,
+                rollingProximityIdKeySizeBytes);
+    }
+
+    /**
+     * Generates a single rolling proximity id using AES(RPIK, PaddedData).
+     *
+     * @param intervalNumber index of 10-minute interval since Epoch.
+     */
+    public RollingProximityId generateId(int intervalNumber) throws CryptoException {
+        return new RollingProximityId(
+                encryptor.encrypt(generatePaddedData(intervalNumber, aesPadding)), /*takeOwnership=*/ true);
+    }
+
+    private ImmutableList<GeneratedRollingProximityId> convertToList(
+            byte[] rawRollingProximityIds, int numIds) {
+        ImmutableList.Builder<GeneratedRollingProximityId> generatedIds = ImmutableList.builder();
+        for (int i = 0; i < numIds; ++i) {
+            generatedIds.add(
+                    GeneratedRollingProximityId.create(
+                            new RollingProximityId(
+                                    Arrays.copyOfRange(
+                                            rawRollingProximityIds, i * AES_BLOCK_SIZE, (i + 1) * AES_BLOCK_SIZE),
+                                    true),
+                            rollingStartIntervalNumber + i));
+        }
+        return generatedIds.build();
+    }
+
+    private static byte[] generatePaddedData(int intervalNumber, byte[] aesPadding) {
+        ByteBuffer paddedData = ByteBuffer.allocate(AES_BLOCK_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+        return addPaddedData(paddedData, intervalNumber, aesPadding).array();
+    }
+
+    protected static byte[] generateAggregatePaddedData(
+            int startIntervalNumber, int numIds, byte[] aesPadding) {
+        ByteBuffer allPaddedData =
+                ByteBuffer.allocate(AES_BLOCK_SIZE * numIds).order(ByteOrder.LITTLE_ENDIAN);
+        for (int i = 0; i < numIds; ++i) {
+            allPaddedData = addPaddedData(allPaddedData, startIntervalNumber + i, aesPadding);
+        }
+        return allPaddedData.array();
+    }
+
+    /**
+     * Padded data is the following sequence of 16 bytes:
+     *
+     * <ol>
+     *   <li>PaddedData[0 - 5]: "EN-RPI".getBytes(UTF_8).
+     *   <li>PaddedData[6 - 11]: 0x000000000000.
+     *   <li>PaddedData[12 - 15]: intervalNumber, uint32 little-endian.
+     * </ol>
+     *
+     * <p>{@code buffer} must be zero filled.
+     */
+    private static ByteBuffer addPaddedData(
+            ByteBuffer buffer, int intervalNumber, byte[] aesPadding) {
+        buffer
+                .put(aesPadding)
+                .position(buffer.position() + AES_BLOCK_SIZE - aesPadding.length - 4); // 4 is Integer.BYTES
+        buffer.putInt(intervalNumber);
+        return buffer;
+    }
+
+    static class PaddedDataCache {
+        private final int baseEnIntervalNumber;
+        private final int size;
+        private final int idsPerKey;
+        private final byte[][] cachedData;
+
+        public static PaddedDataCache createInstance(
+                int baseEnIntervalNumber, int cacheSize, byte[] aesPadding, int idsPerKey) {
+            return new PaddedDataCache(
+                    createPaddedDataCache(baseEnIntervalNumber, cacheSize, aesPadding, idsPerKey),
+                    baseEnIntervalNumber,
+                    cacheSize,
+                    idsPerKey);
+        }
+
+        private PaddedDataCache(
+                byte[][] cachedData, int baseEnIntervalNumber, int size, int idsPerKey) {
+            this.baseEnIntervalNumber = baseEnIntervalNumber;
+            this.cachedData = cachedData;
+            this.size = size;
+            this.idsPerKey = idsPerKey;
+        }
+
+        int getSize() {
+            return size;
+        }
+
+        int getIdsPerKey() {
+            return idsPerKey;
+        }
+
+        @Nullable
+        public byte[] getCachedData(int enIntervalNumber) {
+            int index = getIndex(enIntervalNumber, baseEnIntervalNumber);
+            return (index >= 0 && index < getSize()) ? cachedData[index] : null;
+        }
+
+        private int getIndex(int enIntervalNumber, int baseEnIntervalNumber) {
+            // The given enIntervalNumber should be aligned to idsPerKey.
+            return (((enIntervalNumber - baseEnIntervalNumber) % idsPerKey) == 0)
+                    ? ((enIntervalNumber - baseEnIntervalNumber) / idsPerKey)
+                    : -1;
+        }
+
+        private static byte[][] createPaddedDataCache(
+                int baseEnIntervalNumber, int size, byte[] aesPadding, int idsPerKey) {
+            byte[][] paddedDataSet = new byte[size][];
+            for (int i = 0, intervalNumber = baseEnIntervalNumber;
+                 i < size;
+                 i++, intervalNumber += idsPerKey) {
+                paddedDataSet[i] = generateAggregatePaddedData(intervalNumber, idsPerKey, aesPadding);
+            }
+            return paddedDataSet;
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/TemporaryExposureKeyGenerator.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/TemporaryExposureKeyGenerator.java
new file mode 100644
index 0000000..974179d
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/TemporaryExposureKeyGenerator.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.data.generator;
+
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import java.security.SecureRandom;
+
+/**
+ * Generator for {@link TemporaryExposureKey}.
+ */
+public final class TemporaryExposureKeyGenerator {
+    private TemporaryExposureKeyGenerator() {
+    }
+
+    public static TemporaryExposureKey generateKey(int rollingStartIntervalNumber) {
+        return generateKey(
+                rollingStartIntervalNumber,
+                (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
+    }
+
+    public static TemporaryExposureKey generateKey(
+            int rollingStartIntervalNumber, int rollingPeriod) {
+        SecureRandom random = new SecureRandom();
+        byte[] key = new byte[ContactTracingFeature.temporaryTracingKeySizeBytes()];
+        random.nextBytes(key);
+        return new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                .setKeyData(key)
+                .setRollingStartIntervalNumber(rollingStartIntervalNumber)
+                .setRollingPeriod(rollingPeriod)
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/features/ContactTracingFeature.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/features/ContactTracingFeature.java
new file mode 100644
index 0000000..05c0dda
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/features/ContactTracingFeature.java
@@ -0,0 +1,700 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.features;
+
+import android.bluetooth.le.ScanSettings;
+
+import com.google.common.primitives.Ints;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class contains various constants and feature flags used through the exposure notifications
+ * system. Many of these values should ideally be delivered in a way that they can be updated
+ * without needing to push a new version of code. For example, the method
+ * {@link #partnerPublicKeys()} returns the public keys of all currently active partners using
+ * the system.
+ * FIXME: Implement this as a deserialized object or some sort of key/value store.
+ */
+public class ContactTracingFeature {
+    /**
+     * The input parameter as HKDF info to generate AEMK.
+     */
+    public static String aemkHkdfInfoString() {
+        return "EN-AEMK";
+    }
+
+    /**
+     * The length of associated metadata encryption key in bytes.
+     */
+    public static int associatedMetadataEncryptionKeySizeBytes() {
+        return 16;
+    }
+
+    /**
+     * The length for which a Temporary Tracing Key is valid in multiple of id rolling period.
+     */
+    public static int tkRollingPeriodMultipleOfIdRollingPeriod() {
+        return 144;
+    }
+
+    /**
+     * The length of temporary tracing key in bytes.
+     */
+    public static int temporaryTracingKeySizeBytes() {
+        return 16;
+    }
+
+    /**
+     * The number of rolling periods that can be used before and after an RPIs validity time
+     * during matching.
+     */
+    public static int tkMatchingClockDriftRollingPeriods() {
+        return 12;
+    }
+
+    /**
+     * The length for which an advertising ID is valid.
+     */
+    public static int idRollingPeriodMinutes() {
+        return 10;
+    }
+
+    /**
+     * The length of rolling proximity ID key in bytes.
+     */
+    public static int rollingProximityIdKeySizeBytes() {
+        return 16;
+    }
+
+    /**
+     * The input parameter as HKDF info to generate RPIK.
+     */
+    public static String rpikHkdfInfoString() {
+        return "EN-RPIK";
+    }
+
+    /**
+     * The padded constant for generating rolling proximity IDs
+     */
+    public static String rpidAesPaddedString() {
+        return "EN-RPI";
+    }
+
+    /**
+     * The number of (full) days to keep the contact record data
+     */
+    public static int contactRecordDaysToKeep() {
+        return 14;
+    }
+
+    /**
+     * The transmit power as would be reported by an average iPhone
+     * transmitting at the same power as the Android device running with
+     * ADVERTISE_TX_POWER_LOW
+     */
+    public static int txCalibrationPower() {
+        return 0;
+    }
+
+    /**
+     * Offset to add to the tx calibration power to obtain the tx
+     * power to publish in the payload
+     */
+    public static int txCalibrationPowerToTxPower() {
+        return 0;
+    }
+
+    /**
+     * If enabled, the v1.1 metadata format will be advertised which
+     * includes the calibration confidence value.
+     */
+    public static boolean advertisementMetadataV11() {
+        return true;
+    }
+
+    /**
+     * The confidence value for tx_calibration_power and rssi_offset.
+     * Higher value means higher confidence.
+     * Sees: exposurenotification/src/main/proto/calibration_enums.proto
+     */
+    public static byte calibrationConfidence() {
+        return 0;
+    }
+
+    /**
+     * The length of contact ID.
+     */
+    public static int contactIdLength() {
+        return 16;
+    }
+
+    /**
+     * Value of ADVERTISE_MODE_X enum that should be used.
+     */
+    public static int advertiseMode() {
+        return 1; // BALANCED (250ms)
+    }
+
+    /**
+     * Value of ADVERTISE_TX_POWER_X enum that should be used
+     */
+    public static int advertiseTxPower() {
+        return 1;  // LOW (-15dB)
+    }
+
+    /**
+     * Whether the advertisement should be connectable
+     */
+    public static boolean advertiseConnectable() {
+        return false;
+    }
+
+    /**
+     * Whether the BLE advertiser is legacy mode, will only take effect for O and above
+     */
+    public static boolean advertiseLegacyMode() {
+        return true;
+    }
+
+    /**
+     * The BLE advertising interval, will only take effect for O and above
+     */
+    public static int advertiseInterval() {
+        return 400;  // AdvertisingSetParameters.INTERVAL_MEDIUM
+    }
+
+    /**
+     * Value of ADVERTISE_TX_POWER_X enum that should be used
+     */
+    public static int advertiseTxPowerO() {
+        return -15; // AdvertisingSetParameters.TX_POWER_LOW
+    }
+
+    /**
+     * Whether the BLE advertiser is scannable, will only take effect for O and above
+     */
+    public static boolean advertiseScannable() {
+        return false;
+    }
+
+    /**
+     * Whether using startAdvertisingSet to start advertising for O and above
+     */
+    public static boolean advertiseUseOreoAdvertiser() {
+        return false;
+    }
+
+    /**
+     * File name of signature inside diagnosis keys zip
+     */
+    public static String diagnosisKeySignatureFileName() {
+        return "export.sig";
+    }
+
+    /**
+     * File name of key data binary inside diagnosis keys zip
+     */
+    public static String diagnosisKeyBinFileName() {
+        return "export.bin";
+    }
+
+    /**
+     * Calculates the hash of all keys when provideDiagnosisKeys is called.
+     */
+    public static boolean calculateDiagnosisKeyHash() {
+        return true;
+    }
+
+    /**
+     * Maximum permissible batch size group size of signature
+     */
+    public static int signatureMaxAllowedBatchGroupSize() {
+        return 1_000_000;
+    }
+
+    /**
+     * If enabled, signature verification would accept multiple whole batches
+     */
+    public static boolean enableMultipleWholeBatchSignatureVerification() {
+        return true;
+    }
+
+    /**
+     * Allow signature files to have signatures without a matching public key, or more matching
+     * public keys than signatures, during verification.
+     */
+    public static boolean ignoreUnmatchedSignatures() {
+        return true;
+    }
+
+    /**
+     * A list of partner public keys for diagnosis key signature verification.
+     */
+    public static List<String> partnerPublicKeys() {
+        return Arrays.asList(TEST_PARTNER_PUBLIC_KEYS);
+    }
+
+    /**
+     * each list entry is a string formatted as:
+     * <my.package.name>:<COSIGN_SET>|<COSIGN_SET>|...
+     * <p>
+     * COSIGN_SET:
+     * <PUBLIC_KEY>&<PUBLIC_KEY>&...
+     * <p>
+     * PUBLIC_KEY:
+     * <NAME>,<BASE64KEYVALUE>
+     * <p>
+     * NAME:
+     * <KEY_ID>-<KEY_VERSION>
+     * <p>
+     * KEY_ID and KEY_VERSION:
+     * arbitrary alphanumeric identifiers. Match verification_key_id
+     * and verification_key_version in SignatureInfo fields of the
+     * diagnosis export proto. Should definitely not contain any separator
+     * character used in the flag definition. A name must be defined here, but
+     * verification_key_id may be omitted in the proto if there is only a
+     * single signature in the proto.
+     * <p>
+     * each co-signing set may include multiple key values, where all values
+     * within a set are verified together. multiple co-signing sets may be
+     * specified in order to support key rotations.
+     * <p>
+     * FIXME: Deliver authorized PHA public keys to devices.
+     */
+    private static final String[] TEST_PARTNER_PUBLIC_KEYS = {
+            "com.google.android.apps.exposurenotification:ExampleServer-v1,MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUczyMAkfSeoU77Nmcb1G7t7xyGCAhQqMOIVDFLFas3J+elP7CiotovigCLWj706F07j1EPL27ThRzZl7Ha9uOA==|310-v1,MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE49JY6kekDgxj3Crm4y6kEHdfoKQFSNDM4mV9cgDb+e5nOAw0GeRoRThCu9/wX5wDT2QloFoOjl2pGZHI0f3C3w=="
+    };
+
+    /**
+     * If enabled, will read full header
+     */
+    public static boolean readFullMetadataHeader() {
+        return true;
+    }
+
+    /**
+     * The Settings.Global name for the BLE low latency scan window value.
+     */
+    public static String lowLatencyScanWindowSettingsName() {
+        return "ble_scan_low_latency_window_ms";
+    }
+
+    /**
+     * The Settings.Global name for the BLE low latency scan interval value.
+     */
+    public static String lowLatencyScanIntervalSettingsName() {
+        return "ble_scan_low_latency_interval_ms";
+    }
+
+    /**
+     * The default low latency scan window value to be set when scanning for EN is inactive.
+     */
+    public static int lowLatencyScanWindowDefaultValueMillis() {
+        return 4096;
+    }
+
+    /**
+     * The default low latency scan interval value to be set when scanning for EN is inactive.
+     */
+    public static int lowLatencyScanIntervalDefaultValueMillis() {
+        return 4096;
+    }
+
+    /**
+     * Minimum Scan interval between each scan.
+     */
+    public static int minScanIntervalSeconds() {
+        return 60;
+    }
+
+    /**
+     * Scan interval between each scan.
+     */
+    public static int scanIntervalSeconds() {
+        return 300; // 5 * 60
+    }
+
+    /**
+     * Ratio for scan interval while audio playback. [0.1-1]
+     */
+    public static float scanIntervalRatioWhenAudioPlayback() {
+        return 0.6f;
+    }
+
+    /**
+     * Ratio for scan interval while screen on. [0.1-1]
+     */
+    public static float scanIntervalRatioWhenScreenOn() {
+        return 0.8f;
+    }
+
+    /**
+     * If enabled, the BLE scanning interval and window will be shrunk so that over the course of
+     * the BLE scan, we use all 3 channels instead of only 1.
+     */
+    public static boolean bleScanningAdjustIntervalAndWindowValues() {
+        return true;
+    }
+
+    /**
+     * Offset to add to the measured RSSI to obtain a calibrated RSS.
+     * <p>
+     * The offset in measured rssi between an average iphone and a test
+     * device. Experimentally determined on the most popular devices, and
+     * extrapolated for similar models.
+     * calibrated_rssi = measured_rssi + rssi_offset
+     */
+    public static int rssiOffset() {
+        return 0;
+    }
+
+    /**
+     * Random parameter for scan interval. The actual scan interval will be
+     * scan_interval_seconds - RandomInt() % scan_interval_random_range_seconds,
+     * and by default it could be 210 ~ 300 seconds.
+     */
+    public static int scanIntervalRandomRangeSeconds() {
+        return 90; // 1.5 * 60
+    }
+
+    /**
+     * Scan time for piggyback scan with other app's wake ups.
+     */
+    public static int piggybackScanTimeSeconds() {
+        return 2;
+    }
+
+    /**
+     * Scan time for each scan.
+     */
+    public static int scanTimeSeconds() {
+        return 4;
+    }
+
+    /**
+     * Additional scan time while some specific bluetooth profile is in use. If condition
+     * matches, the total scan time will be scan_time_seconds +
+     * scan_time_extend_for_profile_in_use_seconds,
+     * by default it will be 15 seconds.
+     */
+    public static int scanTimeExtendForProfileInUseSeconds() {
+        return 13;
+    }
+
+    /**
+     * Only extend scan while bluetooth audio streaming or while in calls.
+     */
+    public static boolean extendScanOnlyForBtAudioOrCalls() {
+        return true;
+    }
+
+    /**
+     * Interval for piggyback scan with other app's wake ups.
+     */
+    public static int piggybackScanIntervalSeconds() {
+        return 60;
+    }
+
+    /**
+     * Delay for scheduled piggyback scan task while device idle, in seconds.
+     */
+    public static int piggybackScanDelayWhileDeviceIdleSeconds() {
+        return 5;
+    }
+
+    /**
+     * Delay for scheduled piggyback scan task while screen off, in seconds.
+     */
+    public static int piggybackScanDelayWhileScreenOffSeconds() {
+        return 15;
+    }
+
+    /**
+     * Threshold to determine that if the piggyback task is executed while cpu sleeping. If the
+     * task is delayed for more than this value, then we assume that cpu is during idle mode
+     * recently.
+     */
+    public static int piggybackAccetableMinDelaySeconds() {
+        return 1;
+    }
+
+    /**
+     * Master switch for piggyback scan.
+     */
+    public static boolean enablePiggybackScan() {
+        return false;
+    }
+
+    /**
+     * Whether the BLE scanner accepts only legacy advertising or not, will only take effect for
+     * O and above
+     */
+    public static boolean scanOnlyLegacy() {
+        return false;
+    }
+
+    /**
+     * Hold wakelock for scanning to prevent cpu going to sleep while scanning and delay the stop
+     * scan schedule.
+     */
+    public static boolean wakelockForScan() {
+        return true;
+    }
+
+    /**
+     * Use opportunistic scan between regular scans.
+     */
+    public static boolean enableOpportunisticScan() {
+        return false;
+    }
+
+    /**
+     * Scan mode for contact scan.
+     */
+    public static int scanMode() {
+        return ScanSettings.SCAN_MODE_LOW_LATENCY;
+    }
+
+    /**
+     * By design, every opportunistic scan will reschedule the low latency scan to 5 minutes
+     * later, but it will reschedule too frequently if too many opportunistic scan happen in a in
+     * short period. This constant is used for the minimal reschedule interval.
+     */
+    public static int rescheduleStartScanMinIntervalMs() {
+        return 10000; // 10 * 1000
+    }
+
+    /**
+     * Bucket bounds for risk score latency days.
+     */
+    public static List<Integer> riskScoreLatencyDaysBuckets() {
+        int[] riskScoreLatencyDaysBuckets = new int[]{
+                14,
+                12,
+                10,
+                8,
+                6,
+                4,
+                2,
+        };
+        return Ints.asList(riskScoreLatencyDaysBuckets);
+    }
+
+    /**
+     * Bucket bounds for risk score duration.
+     */
+    public static List<Integer> riskScoreDurationBuckets() {
+        int[] riskScoreDurationBuckets = new int[]{
+                0,
+                5,
+                10,
+                15,
+                20,
+                25,
+                30,
+        };
+        return Ints.asList(riskScoreDurationBuckets);
+    }
+
+    public static List<Integer> riskScoreAttenuationValueBuckets() {
+        int[] riskScoreAttenuationValueBuckets = new int[]{
+                73,
+                63,
+                51,
+                33,
+                27,
+                15,
+                10,
+        };
+        return Ints.asList(riskScoreAttenuationValueBuckets);
+    }
+
+    /**
+     * Minimum estimated time two devices need to be in range of each other before considered a
+     * valid exposure.
+     */
+    public static long minExposureBucketizedDurationSeconds() {
+        return 1;
+    }
+
+    /**
+     * The default minimum attenuation value that must be reached within an exposure duration.
+     */
+    public static int defaultMinExposureAttenuationValue() {
+        return 47; // ~2m distance
+    }
+
+    /**
+     * Max threshold duration between scans to be considered continuous.
+     * <p>
+     * Note that this duration is not bucketized by scan interval, and is strictly
+     * measured against the duration between two consecutive ble scans.
+     */
+    public static int maxInterpolationDurationSeconds() {
+        // Set to be a little larger than scan interval,
+        // which when the phone is in idle mode, can be up
+        // to 9 minutes despite scan interval's value, to
+        // make sure consecutive scans are not considered
+        // individual encounters.
+        return 10 * 60;
+    }
+
+    /**
+     * Maximum duration (in minutes) of ExposureWindow.
+     */
+    public static int maxExposureWindowDurationMinutes() {
+        return 30;
+    }
+
+    /**
+     * Maximum minutes since last scan in ExposureWindow.
+     */
+    public static int maxMinutesSinceLastScan() {
+        return 5;
+    }
+
+    /**
+     * Default minutes since last scan in ExposureWindow, used when previous scan time is unknown.
+     */
+    public static int defaultMinutesSinceLastScan() {
+        return 2;
+    }
+
+    /**
+     * When true, clock drift (embargo) leeway will not be considered when matching near the end
+     * of a key's validity period.
+     */
+    public static boolean ignoreEmbargoPeriodWhenMatchingNearKeyEdges() {
+        return false;
+    }
+
+    /**
+     * Whether to use native code to do matching
+     */
+    public static boolean matchingWithNative() {
+        return true;
+    }
+
+    /**
+     * Pre processing to filter non-matched IDs, then do the matching.
+     */
+    public static boolean useMatchingPreFilter() {
+        return true;
+    }
+
+    /**
+     * Whether to allow RECURSIVE report type in TEK.
+     */
+    public static boolean enableRecursiveTekReportType() {
+        return false;
+    }
+
+    /**
+     * Print more log for matching for debugging
+     */
+    public static boolean moreLogForMatching() {
+        return false;
+    }
+
+    /**
+     * Whether sightings of the same RPI in a single scan instance should be aggregated and only
+     * min attenuation used.
+     */
+    public static boolean aggregateSightingsFromSingleScan() {
+        return true;
+    }
+
+    /**
+     * Maximum number of keys that will be passed to native for matching in one native matching call
+     */
+    public static int matchingWithNativeBufferKeySize() {
+        return 10000;
+    }
+
+    /**
+     * If enabled, will start supporting revocation and change of status for report type.
+     */
+    public static boolean supportRevocationAndChangeStatusReportType() {
+        return true;
+    }
+
+    /**
+     * If enabled, revoked key matches will be stored in the match database (but only when
+     * using ExposureWindows).
+     */
+    public static boolean storeMatchesForRevokedKeys() {
+        return true;
+    }
+
+    /**
+     * Keep existing matching logic, only add a quick filter for non-matched IDs (which should be
+     * 99.9% cases).
+     */
+    public static boolean useMatchingFilter() {
+        return true;
+    }
+
+    /**
+     * Matched RPIs with a TX power higher than this bound will be rejected as outside reasonable
+     * range and potentially malicious.
+     */
+    public static int matchingTxPowerUpperBound() {
+        return 20; // dB
+    }
+
+    /**
+     * Matched RPIs with a TX power lower than this bound will be rejected as outside reasonable
+     * range and potentially malicious.
+     */
+    public static int matchingTxPowerLowerBound() {
+        return -50; // dB
+    }
+
+    /**
+     * Whether the native matching should use the native key file parser
+     */
+    public static boolean useNativeKeyParser() {
+        return true;
+    }
+
+    /**
+     * If enabled, all exposure results will be written at the end of matching instead of
+     * intermittedly throughout the process.
+     */
+    public static boolean storeExposureResultsInTransaction() {
+        return true;
+    }
+
+    /**
+     * The number of times Report Type can be changed.
+     */
+    public static int allowedReportTypeTransitions() {
+        return 1;
+    }
+
+    /**
+     * Whether to enable the compact storage format for contact record
+     * data store (sightings storage). Instead storing all information for
+     * individual received BLE packet, compact format aggregates some fields
+     * while keeping other required packet-specific information unaggregated.
+     */
+    public static boolean contactRecordStoreCompactFormatEnabled() {
+        return false;
+    }
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ContactRecordLookUpTable.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ContactRecordLookUpTable.java
new file mode 100644
index 0000000..f4db3ac
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ContactRecordLookUpTable.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import com.google.samples.exposurenotification.storage.ContactRecordDataStore;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Pre-processes the local scanned IDs
+ */
+public class ContactRecordLookUpTable {
+    private static final int INDEX_TABLE_SIZE = 65536;
+    private final List<byte[]> sortedIds;
+    private final int[] indexTable;
+
+    private ContactRecordLookUpTable(List<byte[]> sortedIds, int[] indexTable) {
+        this.sortedIds = sortedIds;
+        this.indexTable = indexTable;
+    }
+
+    public static ContactRecordLookUpTable create(ContactRecordDataStore contactRecordDataStore) {
+        List<byte[]> sortedIds = sortIds(contactRecordDataStore);
+        return new ContactRecordLookUpTable(sortedIds, indexing(sortedIds));
+    }
+
+    public static ContactRecordLookUpTable createDefault() {
+        return new ContactRecordLookUpTable(new ArrayList<>(), new int[0]);
+    }
+
+    private static List<byte[]> sortIds(ContactRecordDataStore contactRecordDataStore) {
+        List<byte[]> ids = contactRecordDataStore.getAllRawIds();
+        Collections.sort(
+                ids,
+                new Comparator<byte[]>() {
+                    @Override
+                    public int compare(byte[] lhs, byte[] rhs) {
+                        // Compares first 2-byte, 1 - greater than, 0 - equal, -1 - less than
+                        return Integer.compare(idToIndexValue(lhs), idToIndexValue(rhs));
+                    }
+                });
+        return ids;
+    }
+
+    private static int[] indexing(List<byte[]> sortedIds) {
+        int[] indexTable = new int[INDEX_TABLE_SIZE]; // 2-byte prefix as the index of the table.
+        int lastIndex = 0;
+        for (int i = 0; i < sortedIds.size(); i++) {
+            int currentIndex = idToIndexValue(sortedIds.get(i));
+            while (lastIndex < currentIndex) {
+                indexTable[lastIndex++] = i;
+            }
+        }
+        while (lastIndex < INDEX_TABLE_SIZE) {
+            indexTable[lastIndex++] = sortedIds.size();
+        }
+        return indexTable;
+    }
+
+    private static int idToIndexValue(byte[] id) {
+        return ((id[0] & 0xff) << 8) | (id[1] & 0xff);
+    }
+
+    public boolean find(byte[] id) {
+        int index = idToIndexValue(id);
+        int startIndex = (index > 0) ? indexTable[index - 1] : 0;
+        int endIndex = indexTable[index];
+        for (; startIndex < endIndex; startIndex++) {
+            if (Arrays.equals(id, sortedIds.get(startIndex))) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/DailySummaryUtils.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/DailySummaryUtils.java
new file mode 100644
index 0000000..ab40ce0
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/DailySummaryUtils.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import com.google.samples.exposurenotification.nearby.DailySummariesConfig;
+import com.google.samples.exposurenotification.nearby.DailySummariesConfig.DistanceEstimate;
+import com.google.samples.exposurenotification.nearby.DailySummary;
+import com.google.samples.exposurenotification.nearby.DailySummary.DailySummaryBuilder;
+import com.google.samples.exposurenotification.nearby.DailySummary.ExposureSummaryData;
+import com.google.samples.exposurenotification.nearby.DailySummary.ExposureSummaryData.ExposureSummaryDataBuilder;
+import com.google.samples.exposurenotification.ExposureKeyExportProto;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.TekMetadataUtils;
+import com.google.samples.exposurenotification.storage.ExposureWindowProto;
+import com.google.samples.exposurenotification.storage.ScanInstanceProto;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.samples.exposurenotification.matching.ExposureWindowUtils.getTypicalAttenuation;
+import static java.lang.Math.max;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * Utilities for creating daily summaries.
+ */
+public class DailySummaryUtils {
+    public static List<DailySummary> getDailySummaries(
+            DailySummariesConfig config, List<ExposureWindowProto> exposureWindows) {
+        Map<Long, List<ExposureWindowProto>> windows = new HashMap<>();
+        for (ExposureWindowProto window : exposureWindows) {
+            if (!TekMetadataUtils.isTekReportTypeReturnableToClients(window.getTekMetadata())) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "ExposureWindow ignored due to invalid report type %s",
+                                window.getTekMetadata().getReportType());
+                continue;
+            }
+            long windowEpochDays = SECONDS.toDays(window.getEpochSeconds());
+            if (config.getDaysSinceExposureThreshold() > 0) {
+                long todayEpochDays = MILLISECONDS.toDays(System.currentTimeMillis());
+                if (todayEpochDays - windowEpochDays > config.getDaysSinceExposureThreshold()) {
+                    Log.log
+                            .atInfo()
+                            .log(
+                                    "ExposureWindow ignored due to too old, days since Window.Days=%d,"
+                                            + " Config.daysSinceExposureThreshold=%d",
+                                    todayEpochDays - windowEpochDays, config.getDaysSinceExposureThreshold());
+                    continue;
+                }
+            }
+
+            List<ExposureWindowProto> windowProtoList = windows.get(windowEpochDays);
+            if (windowProtoList == null) {
+                windowProtoList = new ArrayList<>();
+                windows.put(windowEpochDays, windowProtoList);
+            }
+            windowProtoList.add(window);
+        }
+
+        if (windows.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        ArrayList<DailySummary> result = new ArrayList<>(windows.size());
+        for (long epochDay : windows.keySet()) {
+            List<ExposureWindowProto> windowProtoList = windows.get(epochDay);
+            SummaryItemScores[] scoresOfAllReports =
+                    new SummaryItemScores
+                            [ExposureKeyExportProto.TemporaryExposureKey.ReportType.values().length];
+            for (int i = 0; i < scoresOfAllReports.length; i++) {
+                scoresOfAllReports[i] = new SummaryItemScores();
+            }
+
+            for (ExposureWindowProto exposureWindowProto : windowProtoList) {
+                SummaryItemScores scores =
+                        scoresOfAllReports[exposureWindowProto.getTekMetadata().getReportType().getNumber()];
+                double windowWeightedDuration =
+                        calculateWindowWeightedDuration(exposureWindowProto, config);
+                double windowScore =
+                        calculateWindowScore(exposureWindowProto, windowWeightedDuration, config);
+                if (windowScore < config.getMinimumWindowScore()) {
+                    Log.log
+                            .atInfo()
+                            .log(
+                                    "ExposureWindow(%d) ignored due to score(%.3f) is lower than"
+                                            + " MinimumWindowScore(%.3f)",
+                                    exposureWindowProto.getEpochSeconds(),
+                                    windowScore,
+                                    config.getMinimumWindowScore());
+                    continue;
+                }
+                scores.sum += windowScore;
+                scores.maximum = max(scores.maximum, windowScore);
+                scores.weightedDurationSum += windowWeightedDuration;
+            }
+
+            result.add(createDailySummary((int) epochDay, scoresOfAllReports));
+        }
+        return result;
+    }
+
+    private static double calculateWindowWeightedDuration(
+            ExposureWindowProto window, DailySummariesConfig config) {
+        int[] durations = new int[DailySummariesConfig.DistanceEstimate.values().length];
+
+        for (ScanInstanceProto scan : window.getScanInstancesList()) {
+            durations[
+                    DistanceEstimate.getDistanceEstimate(getTypicalAttenuation(scan), config)
+                            .ordinal()] +=
+                    scan.getSecondsSinceLastScan();
+        }
+        double result = 0;
+        for (DistanceEstimate distanceEstimate : DistanceEstimate.values()) {
+            result +=
+                    durations[distanceEstimate.ordinal()]
+                            * config.getAttenuationBucketWeights().get(distanceEstimate.ordinal());
+        }
+        return result;
+    }
+
+    private static class SummaryItemScores {
+        private double maximum = 0;
+        private double sum = 0;
+        private double weightedDurationSum = 0;
+    }
+
+    private static DailySummary createDailySummary(
+            int epochDays, SummaryItemScores[] scoresOfAllReports) {
+        DailySummaryBuilder dailySummaryBuilder = new DailySummaryBuilder();
+        dailySummaryBuilder.setDaysSinceEpoch(epochDays);
+        for (ExposureKeyExportProto.TemporaryExposureKey.ReportType reportType :
+                ExposureKeyExportProto.TemporaryExposureKey.ReportType.values()) {
+            dailySummaryBuilder.setReportSummary(
+                    reportType.getNumber(),
+                    createExposureSummaryData(scoresOfAllReports[reportType.getNumber()]));
+        }
+
+        SummaryItemScores scoresOfDaySummary = new SummaryItemScores();
+        for (ExposureKeyExportProto.TemporaryExposureKey.ReportType reportType :
+                ExposureKeyExportProto.TemporaryExposureKey.ReportType.values()) {
+            SummaryItemScores scoreOfReport = scoresOfAllReports[reportType.getNumber()];
+            scoresOfDaySummary.maximum = max(scoresOfDaySummary.maximum, scoreOfReport.maximum);
+            scoresOfDaySummary.sum += scoreOfReport.sum;
+            scoresOfDaySummary.weightedDurationSum += scoreOfReport.weightedDurationSum;
+        }
+        dailySummaryBuilder.setSummaryData(createExposureSummaryData(scoresOfDaySummary));
+        return dailySummaryBuilder.build();
+    }
+
+    private static ExposureSummaryData createExposureSummaryData(SummaryItemScores scores) {
+        return new ExposureSummaryDataBuilder()
+                .setScoreSum(scores.sum)
+                .setMaximumScore(scores.maximum)
+                .setWeightedDurationSum(scores.weightedDurationSum)
+                .build();
+    }
+
+    private static double calculateWindowScore(
+            ExposureWindowProto window, double windowWeightedDuration, DailySummariesConfig config) {
+        return windowWeightedDuration
+                * config.getReportTypeWeights().get(window.getTekMetadata().getReportType().getNumber())
+                * config
+                .getInfectiousnessWeights()
+                .get(window.getTekMetadata().getInfectiousness().getNumber());
+    }
+
+    private DailySummaryUtils() {
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ExposureMatchingTracer.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ExposureMatchingTracer.java
new file mode 100644
index 0000000..a2277e6
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ExposureMatchingTracer.java
@@ -0,0 +1,926 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+import com.google.samples.Clock;
+import com.google.samples.Clock.DefaultClock;
+import com.google.samples.Hex;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType;
+import com.google.samples.exposurenotification.ExposureNotificationEnums.Infectiousness;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.ble.utils.Constants;
+import com.google.samples.exposurenotification.crypto.AesEcbEncryptor;
+import com.google.samples.exposurenotification.crypto.CryptoException;
+import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
+import com.google.samples.exposurenotification.data.BluetoothMetadata;
+import com.google.samples.exposurenotification.data.DayNumber;
+import com.google.samples.exposurenotification.data.GeneratedRollingProximityId;
+import com.google.samples.exposurenotification.data.RollingProximityId;
+import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
+import com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataGenerator;
+import com.google.samples.exposurenotification.data.generator.RollingProximityIdGenerator;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+import com.google.samples.exposurenotification.storage.CloseableIterable;
+import com.google.samples.exposurenotification.storage.CompletedMatchingRequestRecord;
+import com.google.samples.exposurenotification.storage.ContactRecordDataStore;
+import com.google.samples.exposurenotification.storage.ContactRecordDataStore.ContactRecord;
+import com.google.samples.exposurenotification.storage.ExposureResult;
+import com.google.samples.exposurenotification.storage.ExposureResultStorage;
+import com.google.samples.exposurenotification.storage.ExposureWindowProto;
+import com.google.samples.exposurenotification.storage.SightingRecord;
+import com.google.samples.exposurenotification.storage.StorageException;
+import com.google.samples.exposurenotification.storage.TekMetadataRecord;
+
+import org.joda.time.Duration;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import static com.google.samples.exposurenotification.ExposureNotificationEnums.Infectiousness.INFECTIOUSNESS_STANDARD;
+import static com.google.samples.exposurenotification.ble.data.RollingProximityIdValidator.getValidWindowEndIntervalNumber;
+import static com.google.samples.exposurenotification.ble.data.RollingProximityIdValidator.getValidWindowStartIntervalNumber;
+import static com.google.samples.exposurenotification.ble.data.RollingProximityIdValidator.isSightingValid;
+import static com.google.samples.exposurenotification.data.TemporaryExposureKeySupport.getRollingStartIntervalNumber;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+/**
+ * Given a {@link MatchingRequest}, generates {@link ExposureResult}s from sightings seen.
+ *
+ * <p>This class loads {@link TemporaryExposureKey}s from the provided {@link MatchingRequest} and
+ * runs matching against the {@link ContactRecordDataStore}. The following evaluations are done as
+ * part of matching:
+ *
+ * <ul>
+ *   <li>Checking sighted {@link RollingProximityId}s are seen in specified interval number or
+ *       within {@link ContactTracingFeature#tkMatchingClockDriftRollingPeriods()} of either side of
+ *       the interval.
+ *   <li>Filtering exposure to be at least {@link TracingParams#minExposureBucketizedDuration()}.
+ *   <li>Bucketing exposures to be multiples of five minutes. Implementation the previous point and
+ *       this one is implemented in {@link KeyExposureEvaluator}.
+ *   <li>Risk score calculation using {@link RiskScoreCalculator}.
+ * </ul>
+ *
+ * <p>An exposure or encounter is a series of consecutive sightings of {@link RollingProximityId}s
+ * generated by the same diagnosed {@link TemporaryExposureKey}.
+ */
+public class ExposureMatchingTracer {
+    @Deprecated
+    public static final String TOKEN_A = "ATOKEN";
+
+    static final Comparator<SightingRecordWithMetadata> BY_TIME_ASCENDING =
+            (sr1, sr2) -> sr1.sightingRecord().getEpochSeconds() - sr2.sightingRecord().getEpochSeconds();
+    private static final SimpleDateFormat dataFormat =
+            new SimpleDateFormat("MM-dd HH:mm:ss", Locale.ENGLISH);
+
+    private final Context context;
+    private final MatchingRequest matchingRequest;
+    private final String instanceLogTag;
+    private final TekMetadataRecord tekMetadataRecord;
+    private final KeyExposureEvaluator keyExposureEvaluator;
+    private volatile boolean stopRequested = false;
+
+    @VisibleForTesting
+    int diagnosisKeyCount;
+    @VisibleForTesting
+    int processedKeysCount;
+    @VisibleForTesting
+    int matchesFoundCount;
+
+    public ExposureMatchingTracer(Context context, MatchingRequest matchingRequest) {
+        this(context, matchingRequest, DefaultClock.getInstance());
+    }
+
+    @VisibleForTesting
+    public ExposureMatchingTracer(
+            Context context,
+            MatchingRequest matchingRequest,
+            Clock clock) {
+        this.context = context;
+        this.matchingRequest = matchingRequest;
+        this.diagnosisKeyCount = 0;
+        this.processedKeysCount = 0;
+        this.matchesFoundCount = 0;
+        this.instanceLogTag = getTag(matchingRequest);
+        this.tekMetadataRecord =
+                getTekMetadataRecord(
+                        context, matchingRequest.packageName(), matchingRequest.signatureHash());
+        this.keyExposureEvaluator =
+                new KeyExposureEvaluator(
+                        matchingRequest.exposureConfiguration(),
+                        RiskScoreCalculator.create(clock),
+                        TracingParams.builder().build(),
+                        instanceLogTag,
+                        tekMetadataRecord);
+    }
+
+    private String getTag(MatchingRequest matchingRequest) {
+        return String.format("Matching#%04x", matchingRequest.hashCode());
+    }
+
+    /**
+     * If there are still keys to be processed in {@link #trace()}, this method will stop tracing
+     * after the {@link TemporaryExposureKey} that is currently being matched.
+     */
+    public void stopTrace() {
+        stopRequested = true;
+    }
+
+    /**
+     * Returns the request associated with the tracer.
+     */
+    public MatchingRequest getMatchingRequest() {
+        return matchingRequest;
+    }
+
+    /**
+     * Finds exposures to the provided {@link TemporaryExposureKey}s and stores the records of
+     * exposure based on the {@link TracingParams} provided. Returns true if matches were found, false
+     * otherwise.
+     *
+     * <p>See class description for details on what an exposure means.
+     */
+    public boolean trace() throws StorageException, CryptoException {
+        boolean matchesFound;
+        if (ContactTracingFeature.matchingWithNative()) {
+            matchesFound = traceWithNative();
+        } else {
+            if (ContactTracingFeature.useMatchingPreFilter()) {
+                matchesFound = traceWithPreFilter();
+            } else {
+                try (CloseableIterable<TemporaryExposureKey> temporaryExposureKeys =
+                             matchingRequest.diagnosisKeys()) {
+                    matchesFound = traceWithJava(temporaryExposureKeys);
+                } catch (IOException e) {
+                    Log.log.atSevere().withCause(e).log("Failed to read diagnosis keys");
+                    matchesFound = false;
+                }
+            }
+        }
+        persistMatchingRequestRecord();
+        return matchesFound;
+    }
+
+    private void persistMatchingRequestRecord() {
+        Log.log.atInfo().log("%s Persisting matching request record.", instanceLogTag);
+        CompletedMatchingRequestRecord completedMatchingRequestRecord =
+                CompletedMatchingRequestRecord.newBuilder()
+                        .setTemporaryKeysBatchHash(matchingRequest.keyFilesHash())
+                        .setTimestampMillis(System.currentTimeMillis())
+                        .setKeyCount(diagnosisKeyCount > 0 ? diagnosisKeyCount : processedKeysCount)
+                        .setMatchesCount(matchesFoundCount)
+                        .build();
+        // FIXME: Record the completed matching request.
+    }
+
+    private boolean traceWithNative() throws StorageException, CryptoException {
+        Log.log.atInfo().log("%s Native pre-filter started.", instanceLogTag);
+        long startTime = System.currentTimeMillis();
+        try (ContactRecordDataStore contactRecordDataStore = ContactRecordDataStore.open(context)) {
+            List<byte[]> idList = contactRecordDataStore.getAllRawIds();
+            try (MatchingJni matchingJni = new MatchingJni(context, idList.toArray(new byte[0][]))) {
+                Set<TemporaryExposureKey> matchedKeyList;
+                if (ContactTracingFeature.useNativeKeyParser()) {
+                    matchedKeyList = matchingJni.matching(matchingRequest.diagnosisKeyFiles());
+                    diagnosisKeyCount = matchingJni.getLastProcessedKeyCount();
+                } else {
+                    try (CloseableIterable<TemporaryExposureKey> temporaryExposureKeys =
+                                 matchingRequest.diagnosisKeys()) {
+                        Pair<Integer, Set<TemporaryExposureKey>> keysCountPair =
+                                matchingJni.matchingLegacy(temporaryExposureKeys);
+                        diagnosisKeyCount = keysCountPair.first;
+                        matchedKeyList = keysCountPair.second;
+                    } catch (IOException e) {
+                        Log.log.atSevere().withCause(e).log("%s Failed to read diagnosis keys", instanceLogTag);
+                        return false;
+                    }
+                }
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s Native pre-filter found %d (%d) keys with sightings out of %d scan"
+                                        + " records. Spent time: %.3fs",
+                                instanceLogTag,
+                                matchedKeyList.size(),
+                                diagnosisKeyCount,
+                                idList.size(),
+                                (System.currentTimeMillis() - startTime) / 1000f);
+                return traceWithJava(matchedKeyList);
+            }
+        }
+    }
+
+    private boolean traceWithPreFilter() {
+        Log.log.atInfo().log("%s Java pre-filter started.", instanceLogTag);
+        try (ContactRecordDataStore contactRecordDataStore = ContactRecordDataStore.open(context);
+             CloseableIterable<TemporaryExposureKey> temporaryExposureKeys =
+                     matchingRequest.diagnosisKeys()) {
+            RollingProximityIdGenerator.Factory idGeneratorFactory =
+                    new RollingProximityIdGenerator.Factory();
+            List<TemporaryExposureKey> matchedKeyList = new ArrayList<>();
+            ContactRecordLookUpTable contactRecordLookUpTable =
+                    ContactRecordLookUpTable.create(contactRecordDataStore);
+            byte[] reusedEncryptedOutput =
+                    new byte
+                            [(int)
+                            (ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod()
+                                    * ContactTracingFeature.contactIdLength())]; // byte[144 * 16] = byte[2304].
+            AesEcbEncryptor aesEcbEncryptor = AesEcbEncryptor.create();
+            for (TemporaryExposureKey diagnosisKey : temporaryExposureKeys) {
+                diagnosisKeyCount++;
+                List<GeneratedRollingProximityId> rollingProximityIds;
+                // We prefilter based on all the possible RPIs to allow saving of attempted keys in the
+                // actual matching.
+                rollingProximityIds =
+                        idGeneratorFactory
+                                .getInstance(
+                                        aesEcbEncryptor,
+                                        diagnosisKey.getKeyData(),
+                                        diagnosisKey.getRollingStartIntervalNumber(),
+                                        TemporaryExposureKeySupport.getMaxPossibleRollingEndIntervalNumber(
+                                                diagnosisKey),
+                                        (int) ContactTracingFeature.rollingProximityIdKeySizeBytes(),
+                                        ContactTracingFeature.rpikHkdfInfoString(),
+                                        ContactTracingFeature.rpidAesPaddedString())
+                                .generateIds(reusedEncryptedOutput);
+                for (GeneratedRollingProximityId rollingProximityId : rollingProximityIds) {
+                    if (contactRecordLookUpTable.find(rollingProximityId.rollingProximityId().getDirect())) {
+                        matchedKeyList.add(diagnosisKey);
+                        break;
+                    }
+                }
+            }
+
+            Log.log
+                    .atInfo()
+                    .log(
+                            "%s Java pre-filter found %d keys with sightings out of %d total diagnosis keys",
+                            instanceLogTag, matchedKeyList.size(), diagnosisKeyCount);
+            return traceWithJava(matchedKeyList);
+        } catch (StorageException | CryptoException | IOException e) {
+            Log.log
+                    .atWarning()
+                    .withCause(e)
+                    .log("%s Failure while running matching with java pre-filter", instanceLogTag);
+        }
+
+        return false;
+    }
+
+    @SuppressLint("WrongConstant")
+    private boolean traceWithJava(Iterable<TemporaryExposureKey> diagnosisKeys)
+            throws StorageException, CryptoException {
+        Log.log.atInfo().log("%s Java tracing started.", instanceLogTag);
+        if (ContactTracingFeature.moreLogForMatching()) {
+            int keyCount = 0;
+            Log.log.atInfo().log("Dump TEKs for matching");
+            for (TemporaryExposureKey temporaryExposureKey : diagnosisKeys) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s, %d. Rolling start time:%s, rolling period:%d",
+                                Constants.fromIdByteArrayToString(temporaryExposureKey.getKeyData()),
+                                temporaryExposureKey.getRollingStartIntervalNumber(),
+                                fromIntervalNumberToDateString(
+                                        temporaryExposureKey.getRollingStartIntervalNumber()),
+                                temporaryExposureKey.getRollingPeriod());
+                keyCount++;
+                if (keyCount > 10) {
+                    Log.log.atInfo().log("Stop dumping TEKs due to meet maximum dumping size.");
+                    break;
+                }
+            }
+        }
+        RollingProximityIdGenerator.Factory idGeneratorFactory =
+                new RollingProximityIdGenerator.Factory();
+        AssociatedEncryptedMetadataGenerator.Factory metadataGeneratorFactory =
+                new AssociatedEncryptedMetadataGenerator.Factory();
+        boolean foundMatches = false;
+        stopRequested = false;
+        try (ContactRecordDataStore contactRecordDataStore = ContactRecordDataStore.open(context);
+             ExposureResultStorage exposureResultStore = ExposureResultStorage.open(context);
+             AttemptedKeysDataStore attemptedKeysDataStore = AttemptedKeysDataStore.open(context)) {
+            AesEcbEncryptor aesEcbEncryptor = AesEcbEncryptor.create();
+            byte[] reusedEncryptedOutput =
+                    new byte
+                            [(int)
+                            (ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod()
+                                    * ContactTracingFeature.contactIdLength())]; // byte[144 * 16] = byte[2304].
+            ContactRecordLookUpTable contactRecordLookUpTable =
+                    ContactTracingFeature.useMatchingFilter()
+                            ? ContactRecordLookUpTable.create(contactRecordDataStore)
+                            : ContactRecordLookUpTable.createDefault();
+            byte[] tokenRoot =
+                    ExposureResultStorage.encodeTokenRoot(
+                            matchingRequest.packageName(),
+                            matchingRequest.signatureHash(),
+                            matchingRequest.token());
+            byte[] packageRoot =
+                    AttemptedKeysDataStore.encodePackageRoot(
+                            matchingRequest.packageName(), matchingRequest.signatureHash());
+            for (TemporaryExposureKey diagnosisKey : diagnosisKeys) {
+                if (stopRequested) {
+                    Log.log
+                            .atInfo()
+                            .log(
+                                    "%s Matching pre-empted. Processed %d keys.", instanceLogTag, processedKeysCount);
+                    return foundMatches;
+                }
+                ++processedKeysCount;
+
+                if (exposureResultStore.hasResult(tokenRoot, diagnosisKey.getKeyData())) {
+                    if (ContactTracingFeature.supportRevocationAndChangeStatusReportType()
+                            && storeValidReportTransition(
+                            exposureResultStore,
+                            tokenRoot,
+                            diagnosisKey,
+                            tekMetadataRecord.getReportTypeWhenMissing())) {
+                        Log.log
+                                .atInfo()
+                                .log(
+                                        "%s Updated report type of matched diagnosis key because there was a valid"
+                                                + " transition on the previous match.",
+                                        instanceLogTag);
+                    } else {
+                        Log.log
+                                .atInfo()
+                                .log(
+                                        "%s Skipping a diagnosis key because there was a previous match.",
+                                        instanceLogTag);
+                        Log.log
+                                .atVerbose()
+                                .log("%s -- Diagnosis key #%d skipped.", instanceLogTag, processedKeysCount);
+                    }
+                    continue;
+                }
+                if (attemptedKeysDataStore.rollingPeriodChanged(
+                        packageRoot, diagnosisKey.getKeyData(), diagnosisKey.getRollingPeriod())) {
+                    // Do not match keys that were already attempted and rolling period has changed. Rolling
+                    // period should never change for a key. This is kept on a per app but NOT per-token
+                    // basis.
+                    Log.log
+                            .atWarning()
+                            .log(
+                                    "%s Attempted to match TEK with a changed rolling period, skipping.",
+                                    instanceLogTag);
+                    continue;
+                }
+                if (!ContactTracingFeature.enableRecursiveTekReportType()
+                        && diagnosisKey.getReportType() == ReportType.RECURSIVE_VALUE) {
+                    Log.log
+                            .atWarning()
+                            .log(
+                                    "%s Attempted to match TEK with RECURSIVE report type which is not enabled.",
+                                    instanceLogTag);
+                    continue;
+                }
+                if (diagnosisKey.getReportType() >= ReportType.REVOKED_VALUE) {
+                    if (matchingRequest.token().equals(TOKEN_A)
+                            && diagnosisKey.getReportType() == ReportType.REVOKED_VALUE
+                            && ContactTracingFeature.storeMatchesForRevokedKeys()) {
+                        Log.log.atInfo().log("%s Attemping to match TEK with revoked type.", instanceLogTag);
+                    } else {
+                        Log.log
+                                .atWarning()
+                                .log("%s Attempted to match TEK with report type >= REVOKED.", instanceLogTag);
+                        continue;
+                    }
+                }
+
+                // Generates all possibly valid RPIs for this key.
+                List<GeneratedRollingProximityId> rollingProximityIds =
+                        idGeneratorFactory
+                                .getInstance(
+                                        aesEcbEncryptor,
+                                        diagnosisKey.getKeyData(),
+                                        diagnosisKey.getRollingStartIntervalNumber(),
+                                        TemporaryExposureKeySupport.getMaxPossibleRollingEndIntervalNumber(
+                                                diagnosisKey),
+                                        (int) ContactTracingFeature.rollingProximityIdKeySizeBytes(),
+                                        ContactTracingFeature.rpikHkdfInfoString(),
+                                        ContactTracingFeature.rpidAesPaddedString())
+                                .generateIds(reusedEncryptedOutput);
+
+                TemporaryExposureKey diagnosisKeyIgnoringRollingPeriod =
+                        new TemporaryExposureKey.TemporaryExposureKeyBuilder()
+                                .setKeyData(diagnosisKey.getKeyData())
+                                .setRollingStartIntervalNumber(diagnosisKey.getRollingStartIntervalNumber())
+                                .setRollingPeriod(
+                                        (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod())
+                                .setTransmissionRiskLevel(diagnosisKey.getTransmissionRiskLevel())
+                                .setReportType(diagnosisKey.getReportType())
+                                .build();
+                Log.log.atInfo().log("%s Matching scans for all possible RPIs.", instanceLogTag);
+                List<SightingRecordWithMetadata> matchingScansForAllPossibleRpis =
+                        fetchValidSightings(
+                                instanceLogTag,
+                                metadataGeneratorFactory,
+                                contactRecordDataStore,
+                                contactRecordLookUpTable,
+                                rollingProximityIds,
+                                diagnosisKeyIgnoringRollingPeriod,
+                                /*aggregateSightings=*/ false);
+
+                if (!matchingScansForAllPossibleRpis.isEmpty()) {
+                    // Key could possibly match to some RPIs, saving the rolling period with which it was
+                    // attempted.
+                    attemptedKeysDataStore.storeAttemptedKeyRollingPeriod(packageRoot, diagnosisKey);
+                }
+
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s Matching scans for RPIs matching rolling period %d.",
+                                instanceLogTag, diagnosisKey.getRollingPeriod());
+                List<SightingRecordWithMetadata> matchingScansWithNoAggregation =
+                        fetchValidSightings(
+                                instanceLogTag,
+                                metadataGeneratorFactory,
+                                contactRecordDataStore,
+                                contactRecordLookUpTable,
+                                rollingProximityIds.subList(0, diagnosisKey.getRollingPeriod()),
+                                diagnosisKey,
+                                /*aggregateSightings=*/ false);
+
+                List<SightingRecordWithMetadata> matchingScans;
+                if (ContactTracingFeature.aggregateSightingsFromSingleScan()) {
+                    matchingScans =
+                            fetchValidSightings(
+                                    instanceLogTag,
+                                    metadataGeneratorFactory,
+                                    contactRecordDataStore,
+                                    contactRecordLookUpTable,
+                                    rollingProximityIds.subList(0, diagnosisKey.getRollingPeriod()),
+                                    diagnosisKey,
+                                    /*aggregateSightings=*/ ContactTracingFeature.aggregateSightingsFromSingleScan());
+                } else {
+                    matchingScans = matchingScansWithNoAggregation;
+                }
+                ExposureResult exposureResult =
+                        keyExposureEvaluator.findExposures(diagnosisKey, matchingScans);
+
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s Checked matches for diagnosis key #%d against %d matching scans.",
+                                instanceLogTag, processedKeysCount, matchingScans.size());
+                if (exposureResult == null) {
+                    continue;
+                }
+                List<ExposureWindowProto> exposureWindows;
+                if (matchingRequest.token().equals(TOKEN_A)) {
+                    exposureWindows =
+                            keyExposureEvaluator.findExposureWindows(
+                                    diagnosisKey, matchingScansWithNoAggregation);
+                    exposureResult =
+                            exposureResult.toBuilder().addAllExposureWindows(exposureWindows).build();
+                }
+                Log.log.atInfo().log("%s Match found for a diagnosis key.", instanceLogTag);
+                Log.log
+                        .atVerbose()
+                        .log("%s -- Diagnosis key #%d matched.", instanceLogTag, processedKeysCount);
+                foundMatches = true;
+                matchesFoundCount++;
+                exposureResultStore.storeResult(
+                        tokenRoot,
+                        diagnosisKey,
+                        exposureResult,
+                        ContactTracingFeature.storeExposureResultsInTransaction());
+            }
+            if (ContactTracingFeature.storeExposureResultsInTransaction()) {
+                exposureResultStore.commitStoreResultRequestsForTransaction();
+            }
+        }
+        Log.log
+                .atInfo()
+                .log(
+                        "%s Traced %d diagnosis keys and found %d matches.",
+                        instanceLogTag, processedKeysCount, matchesFoundCount);
+        return foundMatches;
+    }
+
+    @VisibleForTesting
+    static boolean storeValidReportTransition(
+            ExposureResultStorage exposureResultStore,
+            byte[] tokenRoot,
+            TemporaryExposureKey diagnosisKey,
+            ReportType reportTypeWhenMissing) {
+        ReportType newType =
+                ExposureWindowUtils.getReportType(diagnosisKey.getReportType(), reportTypeWhenMissing);
+        ExposureResult previousResult =
+                exposureResultStore.getResult(tokenRoot, diagnosisKey.getKeyData());
+        boolean validTransition =
+                isValidTransition(previousResult.getTekMetadata().getReportType(), newType);
+        if (validTransition
+                && previousResult.getReportTypeTransitionCount()
+                < ContactTracingFeature.allowedReportTypeTransitions()) {
+            List<ExposureWindowProto> updatedWindows = new ArrayList<>();
+            for (ExposureWindowProto window : previousResult.getExposureWindowsList()) {
+                updatedWindows.add(
+                        window.toBuilder()
+                                .setTekMetadata(window.getTekMetadata().toBuilder().setReportType(newType).build())
+                                .build());
+            }
+
+            ExposureResult updatedExposureResult =
+                    previousResult.toBuilder()
+                            .setTekMetadata(previousResult.getTekMetadata().toBuilder().setReportType(newType))
+                            .setReportTypeTransitionCount(previousResult.getReportTypeTransitionCount() + 1)
+                            .clearExposureWindows()
+                            .addAllExposureWindows(updatedWindows)
+                            .build();
+
+            return exposureResultStore.storeResult(
+                    tokenRoot, diagnosisKey, updatedExposureResult, /*holdForTransaction=*/ false);
+        }
+        return false;
+    }
+
+    /**
+     * Returns sorted sightings with metadata for the given list of generated ids.
+     */
+    static List<SightingRecordWithMetadata> fetchValidSightings(
+            String instanceLogTag,
+            AssociatedEncryptedMetadataGenerator.Factory metadataGeneratorFactory,
+            ContactRecordDataStore contactRecordDataStore,
+            @Nullable ContactRecordLookUpTable recordPreprocessor,
+            List<GeneratedRollingProximityId> generatedIds,
+            TemporaryExposureKey diagnosisKey,
+            boolean aggregateSightings)
+            throws CryptoException {
+        AssociatedEncryptedMetadataGenerator aemGenerator =
+                metadataGeneratorFactory.getInstance(diagnosisKey);
+        List<SightingRecordWithMetadata> sightingRecordsWithMetadata = new ArrayList<>();
+        for (GeneratedRollingProximityId generatedId : generatedIds) {
+            if (ContactTracingFeature.useMatchingFilter()
+                    && !recordPreprocessor.find(generatedId.rollingProximityId().getDirect())) {
+                continue;
+            }
+            if (ContactTracingFeature.moreLogForMatching()) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s TEK %s matches with ID %s:%d",
+                                instanceLogTag,
+                                Constants.fromIdByteArrayToString(diagnosisKey.getKeyData()),
+                                Constants.fromIdByteArrayToString(generatedId.rollingProximityId().getDirect()),
+                                generatedId.intervalNumber() - diagnosisKey.getRollingStartIntervalNumber());
+            }
+            List<SightingRecord> sightingRecords =
+                    getValidSightingsForRpi(
+                            instanceLogTag,
+                            contactRecordDataStore,
+                            diagnosisKey,
+                            generatedId,
+                            aggregateSightings);
+            for (SightingRecord sightingRecord : sightingRecords) {
+                // This is not a valid "old format" sighting record, skip it.
+                if (!sightingRecord.hasRssi()
+                        || !sightingRecord.hasAssociatedEncryptedMetadata()
+                        || sightingRecord.getAssociatedEncryptedMetadata().size() != 4) {
+                    continue;
+                }
+                BluetoothMetadata metadata =
+                        aemGenerator.decrypt(
+                                generatedId.rollingProximityId(),
+                                AssociatedEncryptedMetadata.create(
+                                        sightingRecord.getAssociatedEncryptedMetadata().toByteArray()));
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s Valid sighting found with TX Power=%s, confidence=%s",
+                                instanceLogTag, metadata.txPower(), metadata.calibrationConfidence().name());
+
+                if (metadata.txPower() <= ContactTracingFeature.matchingTxPowerUpperBound()
+                        && metadata.txPower() >= ContactTracingFeature.matchingTxPowerLowerBound()) {
+                    sightingRecordsWithMetadata.add(
+                            SightingRecordWithMetadata.create(sightingRecord, metadata));
+                } else {
+                    Log.log
+                            .atInfo()
+                            .log(
+                                    "%s TX Power %s is outside reasonable bounds [%s, %s].",
+                                    instanceLogTag,
+                                    metadata.txPower(),
+                                    ContactTracingFeature.matchingTxPowerLowerBound(),
+                                    ContactTracingFeature.matchingTxPowerUpperBound());
+                }
+            }
+        }
+        Collections.sort(sightingRecordsWithMetadata, BY_TIME_ASCENDING);
+        return sightingRecordsWithMetadata;
+    }
+
+    /**
+     * Fetches all valid sightings for a given {@link GeneratedRollingProximityId}.
+     *
+     * <p>A {@link RollingProximityId} sighting is only valid if it falls within a time window on
+     * either side of {@link GeneratedRollingProximityId#intervalNumber()}. The time window is
+     * configured in {@link ContactTracingFeature#tkMatchingClockDriftRollingPeriods()}.
+     *
+     * <p>Given that {@link ContactRecordDataStore} stores sightings keyed by {@link DayNumber}, and
+     * it's sometimes valid to have sightings of a {@link RollingProximityId} across day boundaries,
+     * this method contains logic to include all and valid sightings of the specified {@link
+     * GeneratedRollingProximityId}.
+     *
+     * <p>Sightings are truncated to the first sightings within 2 * {@link
+     * ContactTracingFeature#idRollingPeriodMinutes}, any sightings beyond that (counting from the
+     * first sighting) are discarded as invalid. Legitimate clients should never transmit the RPI for
+     * more than 20 minutes, hence sightings of the same RPI that are farther apart than that must be
+     * replays.
+     */
+    private static List<SightingRecord> getValidSightingsForRpi(
+            String instanceLogTag,
+            ContactRecordDataStore dataStore,
+            TemporaryExposureKey diagnosisKey,
+            GeneratedRollingProximityId generatedId,
+            boolean aggregateSightings) {
+        DayNumber diagnosisKeyDayNumber = TemporaryExposureKeySupport.getDayNumber(diagnosisKey);
+        List<SightingRecord> sightingRecords = new ArrayList<>();
+        if (getValidWindowStartIntervalNumber(generatedId)
+                < getRollingStartIntervalNumber(diagnosisKeyDayNumber)) {
+            ContactRecord prevDayRecord =
+                    dataStore.getRecord(
+                            new DayNumber(diagnosisKeyDayNumber.getValue() - 1),
+                            generatedId.rollingProximityId());
+            if (prevDayRecord != null) {
+                sightingRecords.addAll(prevDayRecord.getValue().getSightingRecordsList());
+            }
+        }
+        ContactRecord todayRecord =
+                dataStore.getRecord(diagnosisKeyDayNumber, generatedId.rollingProximityId());
+        if (todayRecord != null) {
+            sightingRecords.addAll(todayRecord.getValue().getSightingRecordsList());
+        }
+        if (getValidWindowEndIntervalNumber(generatedId, diagnosisKey)
+                > getRollingStartIntervalNumber(diagnosisKeyDayNumber) + 1) {
+            ContactRecord nextDayRecord =
+                    dataStore.getRecord(
+                            new DayNumber(diagnosisKeyDayNumber.getValue() + 1),
+                            generatedId.rollingProximityId());
+            if (nextDayRecord != null) {
+                sightingRecords.addAll(nextDayRecord.getValue().getSightingRecordsList());
+            }
+        }
+
+        // Sort by time ascending.
+        Collections.sort(sightingRecords, (sr1, sr2) -> sr1.getEpochSeconds() - sr2.getEpochSeconds());
+
+        if (ContactTracingFeature.moreLogForMatching()) {
+            Log.log
+                    .atInfo()
+                    .log(
+                            "%s Dump sighting record for RPI %s, %d. Rolling start time:%s",
+                            instanceLogTag,
+                            Constants.fromIdByteArrayToString(generatedId.rollingProximityId().getDirect()),
+                            generatedId.intervalNumber(),
+                            fromIntervalNumberToDateString(generatedId.intervalNumber()));
+            for (SightingRecord sightingRecord : sightingRecords) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s Sighting time:%s rssi=%d, previous_scan_seconds=%d",
+                                instanceLogTag,
+                                fromDurationToDateString(
+                                        Duration.standardSeconds(sightingRecord.getEpochSeconds())),
+                                sightingRecord.getRssi(),
+                                sightingRecord.getPreviousScanEpochSeconds());
+            }
+        }
+
+        // Remove invalid sightings.
+        Iterator<SightingRecord> validCheckIterator = sightingRecords.iterator();
+        while (validCheckIterator.hasNext()) {
+            SightingRecord sightingRecord = validCheckIterator.next();
+            if (!isSightingValid(
+                    generatedId, Duration.standardSeconds(sightingRecord.getEpochSeconds()), diagnosisKey)) {
+                Log.log
+                        .atInfo()
+                        .log("%s Sighting discarded for being found outside of valid window.", instanceLogTag);
+                if (ContactTracingFeature.moreLogForMatching()) {
+                    Log.log
+                            .atInfo()
+                            .log(
+                                    "%s Sighting time:%s is not valid.",
+                                    instanceLogTag,
+                                    fromDurationToDateString(
+                                            Duration.standardSeconds(sightingRecord.getEpochSeconds())));
+                }
+                validCheckIterator.remove();
+            }
+        }
+
+        // Nothing left to do.
+        if (sightingRecords.isEmpty()) {
+            return sightingRecords;
+        }
+
+        int firstSightingTime = sightingRecords.get(0).getEpochSeconds();
+        // We allow for 2 * rotation period for the RPIs to be valid. RPIs outside of this
+        // time (as calculated from the first sighting) are discarded.
+        Iterator<SightingRecord> iterator = sightingRecords.iterator();
+        while (iterator.hasNext()) {
+            SightingRecord sightingRecord = iterator.next();
+            if (sightingRecord.getEpochSeconds() - firstSightingTime
+                    > 2 * MINUTES.toSeconds(ContactTracingFeature.idRollingPeriodMinutes())) {
+                Log.log.atInfo().log("%s Sighting discarded for suspected replay.", instanceLogTag);
+                if (ContactTracingFeature.moreLogForMatching()) {
+                    Log.log
+                            .atInfo()
+                            .log(
+                                    "%s Sighting time:%s discarded for suspected replay.",
+                                    instanceLogTag,
+                                    fromDurationToDateString(
+                                            Duration.standardSeconds(sightingRecord.getEpochSeconds())));
+                }
+                iterator.remove();
+            }
+        }
+
+        if (aggregateSightings) {
+            Iterator<SightingRecord> aggregateSightingsIterator = sightingRecords.iterator();
+            List<SightingRecord> aggregatedSightings = new ArrayList<>();
+            int aggregateIndex = 0;
+            aggregatedSightings.add(aggregateSightingsIterator.next());
+            int previousSeconds = aggregatedSightings.get(0).getEpochSeconds();
+
+            while (aggregateSightingsIterator.hasNext()) {
+                SightingRecord currentSighting = aggregateSightingsIterator.next();
+                int timeDelta = currentSighting.getEpochSeconds() - previousSeconds;
+                if (timeDelta
+                        > 1.5
+                        * (ContactTracingFeature.scanTimeSeconds()
+                        + ContactTracingFeature.scanTimeExtendForProfileInUseSeconds())) {
+                    // New scan.
+                    aggregateIndex += 1;
+                    aggregatedSightings.add(currentSighting);
+                }
+                if (currentSighting.getRssi() > aggregatedSightings.get(aggregateIndex).getRssi()) {
+                    aggregatedSightings.set(aggregateIndex, currentSighting);
+                }
+                previousSeconds = currentSighting.getEpochSeconds();
+            }
+            sightingRecords = aggregatedSightings;
+        }
+
+        if (ContactTracingFeature.moreLogForMatching()) {
+            Log.log.atInfo().log("%s Dump filtered sightings", instanceLogTag);
+            for (SightingRecord sightingRecord : sightingRecords) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s Sighting time:%s rssi=%d, previous_scan_seconds=%d",
+                                instanceLogTag,
+                                fromDurationToDateString(
+                                        Duration.standardSeconds(sightingRecord.getEpochSeconds())),
+                                sightingRecord.getRssi(),
+                                sightingRecord.getPreviousScanEpochSeconds());
+            }
+        }
+
+        return sightingRecords;
+    }
+
+    private static String fromIntervalNumberToDateString(int intervalNumber) {
+        return fromTimeInMsToDateString(
+                MINUTES.toMillis((intervalNumber * ContactTracingFeature.idRollingPeriodMinutes())));
+    }
+
+    private static String fromDurationToDateString(Duration duration) {
+        return fromTimeInMsToDateString(duration.getMillis());
+    }
+
+    private static String fromTimeInMsToDateString(long timeInMs) {
+        return dataFormat.format(timeInMs);
+    }
+
+    private static TekMetadataRecord getTekMetadataRecord(
+            Context context, String packageName, byte[] packageSignature) {
+        return new ClientRecordDataStore(context)
+                .getTekMetadataRecord(packageName, packageSignature);
+    }
+
+    @VisibleForTesting
+    static boolean isValidTransition(ReportType oldReportType, ReportType newReportType) {
+        if (oldReportType == ReportType.SELF_REPORT) {
+            return ImmutableList.of(
+                    ReportType.CONFIRMED_TEST,
+                    ReportType.CONFIRMED_CLINICAL_DIAGNOSIS,
+                    ReportType.REVOKED)
+                    .contains(newReportType);
+        }
+
+        if (oldReportType == ReportType.CONFIRMED_CLINICAL_DIAGNOSIS) {
+            return ImmutableList.of(ReportType.CONFIRMED_TEST, ReportType.REVOKED)
+                    .contains(newReportType);
+        }
+
+        return false;
+    }
+
+    /**
+     * Stores attempted keys and their associated rolling period.
+     *
+     * <p>The store is keyed by PackageNameHash:Signature:TemporaryExposureKeyData.
+     */
+    // FIXME: Replace with a proper data store.
+    private static final class AttemptedKeysDataStore implements AutoCloseable {
+        public static AttemptedKeysDataStore open(Context context) throws StorageException {
+            return new AttemptedKeysDataStore(context);
+        }
+
+        private AttemptedKeysDataStore(Context context) {
+        }
+
+        /**
+         * Encodes package root into byte array.
+         */
+        public static byte[] encodePackageRoot(String packageName, byte[] signatureHash) {
+            // FIXME: Use a better encoding method
+            String packageRoot = String.format("{package:%s,hash:%s}",
+                    packageName,
+                    Hex.bytesToStringLowercase(signatureHash));
+            return packageRoot.getBytes();
+        }
+
+        public void storeAttemptedKeyRollingPeriod(byte[] packageRoot, TemporaryExposureKey diagnosisKey) {
+            // FIXME: Store the attempted key.
+        }
+
+        public boolean rollingPeriodChanged(byte[] packageRoot, byte[] keyData, int rollingPeriod) {
+            // FIXME: Check if the rolling period has changed.
+            return false;
+        }
+
+        @Override
+        public void close() {
+            /* nothing to do */
+        }
+    }
+
+    private static final class ClientRecordDataStore implements AutoCloseable {
+        private final HashMap<String, TekMetadataRecord> store = new HashMap<>();
+
+        public ClientRecordDataStore(Context context) {
+        }
+
+        public TekMetadataRecord getTekMetadataRecord(String packageName, byte[] packageSignature) {
+            String packageKey = encodePackage(packageName, packageSignature);
+            if (store.containsKey(packageKey)) {
+                return store.get(packageKey);
+            }
+            return createDefaultTekMetadataRecord();
+        }
+
+        public static TekMetadataRecord createDefaultTekMetadataRecord() {
+            Infectiousness[] infectiousnessesArray = new Infectiousness[29];
+            Arrays.fill(infectiousnessesArray, INFECTIOUSNESS_STANDARD);
+            return TekMetadataRecord.newBuilder()
+                    .addAllDaysSinceOnsetToInfectiousness(Arrays.asList(infectiousnessesArray))
+                    .build();
+        }
+
+        private String encodePackage(String packageName, byte[] packageSignature) {
+            // FIXME: Use a better encoding method
+            return String.format("{package:%s,hash:%s}",
+                    packageName,
+                    Hex.bytesToStringLowercase(packageSignature));
+        }
+
+        @Override
+        public void close() throws Exception {
+            /* nothing to do */
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ExposureWindowUtils.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ExposureWindowUtils.java
new file mode 100644
index 0000000..5941350
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ExposureWindowUtils.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType;
+import com.google.samples.exposurenotification.ExposureNotificationEnums.Infectiousness;
+import com.google.samples.exposurenotification.storage.ExposureResult;
+import com.google.samples.exposurenotification.storage.ExposureResultStorage;
+import com.google.samples.exposurenotification.storage.ExposureWindowProto;
+import com.google.samples.exposurenotification.storage.ScanInstanceProto;
+import com.google.samples.exposurenotification.storage.StorageException;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.Instant;
+
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.google.samples.exposurenotification.nearby.DiagnosisKeysDataMapping.MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET;
+import static com.google.samples.exposurenotification.nearby.DiagnosisKeysDataMapping.SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS;
+import static com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType.REPORT_TYPE_UNKNOWN;
+import static com.google.samples.exposurenotification.matching.ExposureMatchingTracer.TOKEN_A;
+import static java.lang.Math.min;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * Utilities for get exposure windows.
+ */
+public class ExposureWindowUtils {
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+
+    @TargetApi(VERSION_CODES.KITKAT)
+    public static List<ExposureWindowProto> getExposureWindows(
+            String callingPackage, byte[] signatureHash, ExposureResultStorage exposureResultStorage) {
+        List<ExposureWindowProto> exposureWindows = new ArrayList<>();
+
+        try {
+            List<ExposureResult> entries =
+                    exposureResultStorage.getAll(callingPackage, signatureHash, TOKEN_A);
+            for (ExposureResult exposureResult : entries) {
+                exposureWindows.addAll(exposureResult.getExposureWindowsList());
+            }
+        } catch (StorageException e) {
+            throw new RuntimeException("FAILED_DISK_IO", e);
+        }
+        Collections.shuffle(exposureWindows, SECURE_RANDOM);
+        return exposureWindows;
+    }
+
+    /**
+     * Returns epoch timestamp indicating the beginning of the given timezone day for the provided
+     * epoch.
+     */
+    public static long epochToDay(int epoch, DateTimeZone dateTimeZone) {
+        Instant instant = new Instant(SECONDS.toMillis(epoch));
+        DateTime date = instant.toDateTime(dateTimeZone).withTime(0, 0, 0, 0);
+        return date.getMillis();
+    }
+
+    public static int getTypicalAttenuation(ScanInstanceProto scanInstanceProto) {
+        double sum = 0;
+        for (int attenuation : scanInstanceProto.getAttenuationsList()) {
+            sum += attenuation;
+        }
+        return (int) Math.round(sum / scanInstanceProto.getAttenuationsCount());
+    }
+
+    public static int getMinAttenuation(ScanInstanceProto scanInstanceProto) {
+        int min = Integer.MAX_VALUE;
+        for (int attenuation : scanInstanceProto.getAttenuationsList()) {
+            min = min(min, attenuation);
+        }
+        return min;
+    }
+
+    public static Infectiousness getInfectiousness(
+            int daysSinceOnsetOfSymptoms,
+            List<Infectiousness> daysSinceOnsetToInfectiousness,
+            Infectiousness defaultInfectiousness) {
+        if (Math.abs(daysSinceOnsetOfSymptoms) > MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET) {
+            return defaultInfectiousness;
+        }
+        if (daysSinceOnsetToInfectiousness.size() != SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS) {
+            return defaultInfectiousness;
+        }
+        return daysSinceOnsetToInfectiousness.get(
+                daysSinceOnsetOfSymptoms + MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET);
+    }
+
+    public static ReportType getReportType(
+            @com.google.samples.exposurenotification.nearby.ReportType int reportTypeValue,
+            ReportType defaultReportType) {
+        ReportType reportType = ReportType.forNumber(reportTypeValue);
+        if (reportType == null || REPORT_TYPE_UNKNOWN.equals(reportType)) {
+            reportType = defaultReportType;
+        }
+        return reportType;
+    }
+
+    private ExposureWindowUtils() {
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/KeyExposureEvaluator.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/KeyExposureEvaluator.java
new file mode 100644
index 0000000..24f8326
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/KeyExposureEvaluator.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.collect.Iterables;
+import com.google.samples.exposurenotification.nearby.ExposureConfiguration;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType;
+import com.google.samples.exposurenotification.ExposureNotificationEnums.Infectiousness;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+import com.google.samples.exposurenotification.storage.ExposureRecord;
+import com.google.samples.exposurenotification.storage.ExposureResult;
+import com.google.samples.exposurenotification.storage.ExposureWindowProto;
+import com.google.samples.exposurenotification.storage.ScanInstanceProto;
+import com.google.samples.exposurenotification.storage.SightingRecord;
+import com.google.samples.exposurenotification.storage.TekMetadata;
+import com.google.samples.exposurenotification.storage.TekMetadataRecord;
+
+import org.joda.time.Duration;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+/**
+ * Handles the evaluation of exposure for a single diagnosis key and its sightings.
+ *
+ * <p>See {@link #findExposures(TemporaryExposureKey, List)}.
+ */
+final class KeyExposureEvaluator {
+    private final ExposureConfiguration exposureConfiguration;
+    private final RiskScoreCalculator riskScoreCalculator;
+    private final TracingParams tracingParams;
+    private final String instanceLogTag;
+    private final TekMetadataRecord tekMetadataRecord;
+
+    /**
+     * @param exposureConfiguration exposure configuration used to run evaluations with.
+     * @param riskScoreCalculator   risk score calculator to generate scores with
+     * @param tracingParams         tracing parameters for filtering exposures based on sightings
+     * @param instanceLogTag        log tag associated with matching job. See {@link ExposureMatchingTracer}.
+     * @param tekMetadataRecord     tek data mapping for creating {@link ExposureWindowProto}.
+     */
+    KeyExposureEvaluator(
+            ExposureConfiguration exposureConfiguration,
+            RiskScoreCalculator riskScoreCalculator,
+            TracingParams tracingParams,
+            String instanceLogTag,
+            TekMetadataRecord tekMetadataRecord) {
+        this.exposureConfiguration = exposureConfiguration;
+        this.riskScoreCalculator = riskScoreCalculator;
+        this.tracingParams = tracingParams;
+        this.instanceLogTag = instanceLogTag;
+        this.tekMetadataRecord = tekMetadataRecord;
+    }
+
+    /**
+     * Finds exposure windows given the time-sorted sightings seen from the provided {@link
+     * TemporaryExposureKey}.
+     *
+     * <p>Exposures to one TEK are split in windows of 30 minutes, so a given TEK may lead to several
+     * exposure windows if beacon sightings for it spanned more than 30 minutes.
+     *
+     * @param diagnosisKey    The diagnosis key whose rolling proximity identifiers were sighted in the
+     *                        provided list of sightings.
+     * @param sightingRecords a list of rolling proximity id sightings.
+     * @return list of exposure windows for the diagnosisKey.
+     */
+    List<ExposureWindowProto> findExposureWindows(
+            TemporaryExposureKey diagnosisKey, List<SightingRecordWithMetadata> sightingRecords) {
+        List<ExposureWindowProto> exposureWindows = new ArrayList<>();
+
+        ExposureWindowProto.Builder currentExposureWindowBuilder = null;
+        int previousSightingEpochSeconds = 0;
+        Infectiousness infectiousness =
+                ExposureWindowUtils.getInfectiousness(
+                        diagnosisKey.getDaysSinceOnsetOfSymptoms(),
+                        tekMetadataRecord.getDaysSinceOnsetToInfectiousnessList(),
+                        tekMetadataRecord.getInfectiousnessWhenDaysSinceOnsetMissing());
+        ReportType reportType =
+                ExposureWindowUtils.getReportType(
+                        diagnosisKey.getReportType(), tekMetadataRecord.getReportTypeWhenMissing());
+        for (SightingRecordWithMetadata sightingRecordWithMetadata : sightingRecords) {
+            if (shouldStartNewExposureWindow(sightingRecordWithMetadata, currentExposureWindowBuilder)) {
+                if (currentExposureWindowBuilder != null) {
+                    exposureWindows.add(currentExposureWindowBuilder.build());
+                }
+                currentExposureWindowBuilder =
+                        ExposureWindowProto.newBuilder()
+                                .setEpochSeconds(sightingRecordWithMetadata.sightingRecord().getEpochSeconds())
+                                .setTekMetadata(
+                                        TekMetadata.newBuilder()
+                                                .setInfectiousness(infectiousness)
+                                                .setReportType(reportType)
+                                                .setCalibrationConfidence(
+                                                        sightingRecordWithMetadata.metadata().calibrationConfidence()));
+            }
+            if (sightingInTheSameScanInstance(sightingRecordWithMetadata, previousSightingEpochSeconds)
+                    && currentExposureWindowBuilder.getScanInstancesCount() > 0) {
+                // Add to the last existing scan instance.
+                currentExposureWindowBuilder.setScanInstances(
+                        currentExposureWindowBuilder.getScanInstancesCount() - 1,
+                        currentExposureWindowBuilder
+                                .getScanInstances(currentExposureWindowBuilder.getScanInstancesCount() - 1)
+                                .toBuilder()
+                                .addAttenuations(sightingRecordWithMetadata.attenuationValue()));
+            } else {
+                // Create a new ScanInstance.
+                currentExposureWindowBuilder.addScanInstances(
+                        ScanInstanceProto.newBuilder()
+                                .addAttenuations(sightingRecordWithMetadata.attenuationValue())
+                                .setSecondsSinceLastScan(
+                                        sightingRecordWithMetadata.secondsSinceLastScan(
+                                                (int) ContactTracingFeature.maxMinutesSinceLastScan(),
+                                                (int) ContactTracingFeature.defaultMinutesSinceLastScan())));
+            }
+            previousSightingEpochSeconds = sightingRecordWithMetadata.sightingRecord().getEpochSeconds();
+        }
+
+        if (currentExposureWindowBuilder != null) {
+            exposureWindows.add(currentExposureWindowBuilder.build());
+        }
+        return exposureWindows;
+    }
+
+    /**
+     * Finds exposures given the time-sorted sightings seen from the provided {@link
+     * TemporaryExposureKey}.
+     *
+     * <p>Exposures are defined as consecutive sightings where the time between each sighting is not
+     * longer than {@link TracingParams#maxInterpolationDuration()} and the exposure duration is not
+     * shorter than {@link TracingParams#minExposureBucketizedDuration()}.
+     *
+     * <p>Exposure durations are rounded into five minute intervals before being evaluated against
+     * {@link TracingParams#minExposureBucketizedDuration()}.
+     *
+     * @param diagnosisKey    The diagnosis key whose rolling proximity identifiers were sighted in the
+     *                        provided list of sightings.
+     * @param sightingRecords a list of rolling proximity id sightings.
+     * @return exposure result containing list of exposures.
+     */
+    @Nullable
+    ExposureResult findExposures(
+            TemporaryExposureKey diagnosisKey, List<SightingRecordWithMetadata> sightingRecords) {
+        if (sightingRecords.isEmpty()) {
+            Log.log.atInfo().log("%s No sighting records.", instanceLogTag);
+            return null;
+        }
+        List<ExposureRecord> exposureRecords = new ArrayList<>();
+        // The average is computed using time spent at
+        // attenuation levels as weights. The attenuation level between two scans is
+        // the attenuation level of the first scan. The first and last scans get extended by half of
+        // scanInterval. Consider:
+        //
+        // scanInterval = 6
+        // t=00, attenuation=1
+        // t=10, attenuation=4
+        // t=50, attenuation=5
+        //
+        // The resulting attenuation value is:
+        //
+        // (6/2 * 1 + (10 - 0) * 1 + (50 - 10) * 4 + 6/2 * 5) / (3 + 10 + 40 +3) = 188 / 56 = 3.36
+        //
+        // The result is rounded to 3.
+
+        int exposureFirstSightingSeconds = sightingRecords.get(0).sightingRecord().getEpochSeconds();
+        List<TimeAndAttenuation> timeAndAttenuations = new ArrayList<>();
+
+        int maxRiskScore = 0;
+        int totalRiskScore = 0;
+        List<Integer> totalTimesBelowBetweenAndAbove = Arrays.asList(0, 0, 0);
+
+        for (int i = 0; i < sightingRecords.size(); ++i) {
+            SightingRecord currentScan = sightingRecords.get(i).sightingRecord();
+            timeAndAttenuations.add(
+                    TimeAndAttenuation.create(
+                            currentScan.getEpochSeconds(), sightingRecords.get(i).attenuationValue()));
+
+            if (isEndOfPossibleExposureSightings(
+                    i, sightingRecords, tracingParams.maxInterpolationDuration())) {
+                Log.log.atVerbose().log("%s Found a series of sightings.", instanceLogTag);
+                // 5 minutes bucketization, as defined in the API.
+                Duration bucketizedDuration =
+                        bucketizeDuration(
+                                Duration.standardSeconds(
+                                        currentScan.getEpochSeconds() - exposureFirstSightingSeconds),
+                                Duration.standardMinutes(5));
+
+                List<Period> periods =
+                        computePeriods(timeAndAttenuations, tracingParams.scanInterval().getStandardSeconds());
+                int weightedAttenuation = weightAttenuationOfPeriods(periods);
+                int[] durationAtAttenuationThresholds =
+                        exposureConfiguration.getDurationAtAttenuationThresholds();
+                List<Integer> timesBelowBetweenAndAbove =
+                        getTimeBelowBetweenAndAbove(
+                                periods,
+                                durationAtAttenuationThresholds[0],
+                                durationAtAttenuationThresholds[1],
+                                tracingParams.interpolationEnabled());
+
+                Log.log
+                        .atInfo()
+                        .log(
+                                "%s Bucketed duration=%dm >= min_duration=%dm ? %b.",
+                                instanceLogTag,
+                                bucketizedDuration.getStandardMinutes(),
+                                tracingParams.minExposureBucketizedDuration().getStandardMinutes(),
+                                !bucketizedDuration.isShorterThan(tracingParams.minExposureBucketizedDuration()));
+                if (!bucketizedDuration.isShorterThan(tracingParams.minExposureBucketizedDuration())) {
+                    Log.log.atVerbose().log("%s Found exposure.", instanceLogTag);
+                    ExposureRecord exposureRecord =
+                            ExposureRecord.newBuilder()
+                                    .setAttenuationValue(weightedAttenuation)
+                                    .setDurationSeconds((int) bucketizedDuration.getStandardSeconds())
+                                    .addAllAttenuationDurations(timesBelowBetweenAndAbove)
+                                    .setTransmissionRiskLevel(diagnosisKey.getTransmissionRiskLevel())
+                                    .build();
+
+                    long dateMillisSinceEpoch =
+                            Duration.standardMinutes(
+                                    diagnosisKey.getRollingStartIntervalNumber()
+                                            * ContactTracingFeature.idRollingPeriodMinutes())
+                                    .getMillis();
+                    int riskScore =
+                            riskScoreCalculator.calculateRiskScore(
+                                    diagnosisKey, exposureRecord, dateMillisSinceEpoch, exposureConfiguration);
+                    maxRiskScore = Math.max(riskScore, maxRiskScore);
+                    totalRiskScore += riskScore;
+                    for (int bin = 0; bin < totalTimesBelowBetweenAndAbove.size(); bin++) {
+                        totalTimesBelowBetweenAndAbove.set(
+                                bin, totalTimesBelowBetweenAndAbove.get(bin) + timesBelowBetweenAndAbove.get(bin));
+                    }
+
+                    exposureRecords.add(exposureRecord.toBuilder().setRiskScore(riskScore).build());
+                }
+                if (i + 1 == sightingRecords.size()) {
+                    break;
+                }
+                exposureFirstSightingSeconds =
+                        sightingRecords.get(i + 1).sightingRecord().getEpochSeconds();
+                timeAndAttenuations.clear();
+            }
+        }
+        if (exposureRecords.isEmpty()) {
+            return null;
+        }
+        ReportType reportType =
+                ExposureWindowUtils.getReportType(
+                        diagnosisKey.getReportType(), tekMetadataRecord.getReportTypeWhenMissing());
+        return ExposureResult.newBuilder()
+                .addAllExposureRecords(exposureRecords)
+                .setMaxRiskScore(maxRiskScore)
+                .setDateMillisSinceEpoch(
+                        Duration.standardMinutes(
+                                diagnosisKey.getRollingStartIntervalNumber()
+                                        * ContactTracingFeature.idRollingPeriodMinutes())
+                                .getMillis())
+                .setTracingParamsRecord(tracingParams.toTracingParamsRecord())
+                .setTotalRiskScore(totalRiskScore)
+                .addAllAttenuationDurations(totalTimesBelowBetweenAndAbove)
+                .setTekMetadata(TekMetadata.newBuilder().setReportType(reportType))
+                .setReportTypeTransitionCount(0)
+                .build();
+    }
+
+    private static boolean shouldStartNewExposureWindow(
+            SightingRecordWithMetadata sightingRecordWithMetadata,
+            ExposureWindowProto.Builder currentExposureWindowBuilder) {
+        return currentExposureWindowBuilder == null
+                || sightingRecordWithMetadata.sightingRecord().getEpochSeconds()
+                - currentExposureWindowBuilder.getEpochSeconds()
+                >= MINUTES.toSeconds(ContactTracingFeature.maxExposureWindowDurationMinutes());
+    }
+
+    private static boolean sightingInTheSameScanInstance(
+            SightingRecordWithMetadata sightingRecordWithMetadata, int previousSightingEpochSeconds) {
+        // Sightings are considered to be in the same scan if they are separate by a relatively short
+        // duration. The exact value here is not important, as scanning durations are at the order of
+        // seconds vs. time between scans are at the order of minutes. We use 1.5 * max scan duration as
+        // a simple heuristic.
+        return sightingRecordWithMetadata.sightingRecord().getEpochSeconds()
+                - previousSightingEpochSeconds
+                <= 1.5
+                * (ContactTracingFeature.scanTimeSeconds()
+                + ContactTracingFeature.scanTimeExtendForProfileInUseSeconds());
+    }
+
+    /**
+     * Returns a list of {@link Period}s with duration > 0.
+     */
+    private static List<Period> computePeriods(
+            List<TimeAndAttenuation> timeAndAttenuations, long scanInterval) {
+        // Add fake start and end boundary scans with the same attenuation as first and last ones.
+        int timeMargin = (int) (scanInterval / 2);
+        timeAndAttenuations.add(
+                0,
+                TimeAndAttenuation.create(
+                        timeAndAttenuations.get(0).time() - timeMargin,
+                        timeAndAttenuations.get(0).attenuation()));
+        timeAndAttenuations.add(
+                TimeAndAttenuation.create(
+                        Iterables.getLast(timeAndAttenuations).time() + timeMargin,
+                        Iterables.getLast(timeAndAttenuations).attenuation()));
+
+        List<Period> periods = new ArrayList<>();
+
+        TimeAndAttenuation previous = timeAndAttenuations.get(0);
+        for (TimeAndAttenuation timeAndAttenuation : timeAndAttenuations) {
+            // Nothing to do for 0-length segments.
+            if (timeAndAttenuation.time().equals(previous.time())) {
+                continue;
+            }
+            periods.add(Period.create(previous, timeAndAttenuation));
+            previous = timeAndAttenuation;
+        }
+
+        return periods;
+    }
+
+    /**
+     * Calculates a time-weighted attenuation value.
+     */
+    private static int weightAttenuationOfPeriods(List<Period> periods) {
+        double attenuationSum = 0;
+        double durationSum = 0;
+        for (Period period : periods) {
+            attenuationSum += period.scan1().attenuation() * period.duration();
+            durationSum += period.duration();
+        }
+        return (int) Math.round(attenuationSum / durationSum);
+    }
+
+    private static List<Integer> getTimeBelowBetweenAndAbove(
+            List<Period> periods, int thresholdLow, int thresholdHigh, boolean interpolate) {
+        Pair<Integer, Integer> timeBelowAndAboveOfLow =
+                getTimeBelowAndAbove(periods, thresholdLow, interpolate);
+        Pair<Integer, Integer> timeBelowAndAboveOfHigh =
+                getTimeBelowAndAbove(periods, thresholdHigh, interpolate);
+
+        int timeBelowLow = timeBelowAndAboveOfLow.first;
+        int timeAboveHigh = timeBelowAndAboveOfHigh.second;
+        int totalTime = timeBelowAndAboveOfLow.first + timeBelowAndAboveOfLow.second;
+        // Written explicitly to avoid confusion.
+        int timeBetween = totalTime - timeBelowLow - timeAboveHigh;
+
+        return Arrays.asList(timeBelowLow, timeBetween, timeAboveHigh);
+    }
+
+    /**
+     * Calculates duration below and above the given threshold.
+     *
+     * <p>Set {@code interpolate} to {@code true} to use linear interpolation of the attenuation
+     * values.
+     */
+    private static Pair<Integer, Integer> getTimeBelowAndAbove(
+            List<Period> periods, int threshold, boolean interpolate) {
+        int timeAboveThreshold = 0;
+        int timeBelowThreshold = 0;
+        for (Period period : periods) {
+
+            int timeCross = period.calculateTimeCross(threshold, interpolate);
+
+            // No cross in the interval.
+            if (timeCross <= period.scan1().time() || timeCross >= period.scan2().time()) {
+                if (period.scan1().attenuation() >= threshold) {
+                    // The entire interval is above the threshold.
+                    timeAboveThreshold += period.duration();
+                } else {
+                    // The entire interval is below threshold.
+                    timeBelowThreshold += period.duration();
+                }
+            } else {
+                // Cross in the interval, see how much is above and below.
+                int leftTime = timeCross - period.scan1().time();
+                int rightTime = period.scan2().time() - timeCross;
+                if (period.scan1().attenuation() >= threshold) {
+                    // The left hand side is above threshold
+                    timeAboveThreshold += leftTime;
+                    timeBelowThreshold += rightTime;
+                } else {
+                    // The right hand side is above the threshold
+                    timeAboveThreshold += rightTime;
+                    timeBelowThreshold += leftTime;
+                }
+            }
+        }
+        return new Pair<>(timeBelowThreshold, timeAboveThreshold);
+    }
+
+    /**
+     * Returns true if the next {@link SightingRecord} is too far away in time from current {@link
+     * SightingRecord}.
+     *
+     * @param index                    current index in question for end of sightings series or not.
+     * @param sightingRecords          scan records in ascending order of scan time.
+     * @param maxInterpolationDuration duration threshold for two {@link SightingRecord}s being too
+     *                                 far away to be a part of the same exposure.
+     */
+    private static boolean isEndOfPossibleExposureSightings(
+            int index,
+            List<SightingRecordWithMetadata> sightingRecords,
+            Duration maxInterpolationDuration) {
+        // Last item always marks end of exposure sightings.
+        if (index + 1 == sightingRecords.size()) {
+            return true;
+        }
+        return Duration.standardSeconds(
+                sightingRecords.get(index + 1).sightingRecord().getEpochSeconds()
+                        - sightingRecords.get(index).sightingRecord().getEpochSeconds())
+                .isLongerThan(maxInterpolationDuration);
+    }
+
+    /**
+     * For every observation of a sighting (which occurs at {@code scanInterval}, our best estimate
+     * for the duration of exposure is {@code scanInterval}. Therefore, to calculate exposure
+     * duration, {@code scanInterval} must be added and the estimate must be rounded to the nearest
+     * {@code scanInterval} increment.
+     */
+    private static Duration bucketizeDuration(Duration sightedScanDuration, Duration scanInterval) {
+        Duration estimatedExposureDuration = sightedScanDuration.plus(scanInterval);
+        long mod = estimatedExposureDuration.getStandardSeconds() % scanInterval.getStandardSeconds();
+        if (mod <= scanInterval.getStandardSeconds() / 2) {
+            return estimatedExposureDuration.minus(Duration.standardSeconds(mod));
+        }
+        return estimatedExposureDuration.plus(scanInterval.minus(Duration.standardSeconds(mod)));
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingJni.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingJni.java
new file mode 100644
index 0000000..3f8d3b5
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingJni.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import android.content.Context;
+import android.util.Pair;
+import com.google.common.collect.ImmutableSet;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.samples.exposurenotification.ExposureKeyExportProto;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.data.fileformat.TemporaryExposureKeyConverter;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Implements generate id and key matching under native code. */
+public class MatchingJni implements AutoCloseable {
+
+    private static native long initNative(byte[][] bleScanResults);
+
+    /**
+     * Returns the {@link ExposureKeyExportProto.TemporaryExposureKey} array. Each element is the a
+     * serialized byte array and can be converted by {@link
+     * ExposureKeyExportProto.TemporaryExposureKey#parseFrom(byte[])}
+     */
+    private static native byte[][] matchingNative(long nativePtr, String[] keyFiles);
+
+    private static native int[] matchingLegacyNative(
+            long nativePtr, byte[][] tempKeys, int[] rollingStartIntervalNumber, int currentKeyIndex);
+
+    /**
+     * Returns the processed key count which are filtered by invoking {@link #matchingNative}. If the
+     * {@code nativePtr} is invalid, returns -1.
+     */
+    private static native int lastProcessedKeyCountNative(long nativePtr);
+
+    private static native void releaseNative(long nativePtr);
+
+    private final long nativePtr;
+
+    public static boolean loadNativeLibrary(Context context) {
+        System.loadLibrary("matching");
+        return true;
+    }
+
+    public MatchingJni(Context context, byte[][] bleScanResults) {
+        loadNativeLibrary(context);
+        this.nativePtr = initNative(bleScanResults);
+        Log.log.atInfo().log("MatchingJni get native ptr %d", nativePtr);
+    }
+
+    public ImmutableSet<TemporaryExposureKey> matching(List<String> keyFiles) {
+        byte[][] protoArray = matchingNative(nativePtr, keyFiles.toArray(new String[0]));
+        if (protoArray == null) {
+            Log.log.atInfo().log("MatchingJni get nullable key set from native.");
+            return ImmutableSet.of();
+        }
+
+        ImmutableSet.Builder<TemporaryExposureKey> keySet = new ImmutableSet.Builder<>();
+        for (byte[] proto : protoArray) {
+            try {
+                ExposureKeyExportProto.TemporaryExposureKey key =
+                        ExposureKeyExportProto.TemporaryExposureKey.parseFrom(proto);
+                TemporaryExposureKey convertedKey = new TemporaryExposureKeyConverter().convert(key);
+                if (convertedKey == null) {
+                    Log.log.atWarning().log("MatchingJni failed to convert the proto key.");
+                    continue;
+                }
+                keySet.add(convertedKey);
+            } catch (InvalidProtocolBufferException e) {
+                Log.log.atWarning().withCause(e).log("MatchingJni failed to parse the proto byte.");
+            }
+        }
+        return keySet.build();
+    }
+
+    public Pair<Integer, Set<TemporaryExposureKey>> matchingLegacy(
+            Iterable<TemporaryExposureKey> temporaryExposureKeys) {
+        int matchingWithNativeBufferKeySize =
+                (int) ContactTracingFeature.matchingWithNativeBufferKeySize();
+        Set<TemporaryExposureKey> result = new HashSet<>();
+        TemporaryExposureKey[] keys = new TemporaryExposureKey[matchingWithNativeBufferKeySize];
+        int keyCount = 0;
+        byte[][] keyBuffer = new byte[matchingWithNativeBufferKeySize][];
+        int[] rollingStartIntervalNumberBuffer = new int[matchingWithNativeBufferKeySize];
+        int currentKeyIndex = 0;
+        for (TemporaryExposureKey diagnosisKey : temporaryExposureKeys) {
+            keyCount++;
+            keyBuffer[currentKeyIndex] = diagnosisKey.getKeyData();
+            rollingStartIntervalNumberBuffer[currentKeyIndex] =
+                    diagnosisKey.getRollingStartIntervalNumber();
+            keys[currentKeyIndex] = diagnosisKey;
+            currentKeyIndex++;
+            if (currentKeyIndex >= matchingWithNativeBufferKeySize) {
+                processingResult(
+                        matchingLegacyNative(
+                                nativePtr, keyBuffer, rollingStartIntervalNumberBuffer, currentKeyIndex),
+                        keys,
+                        result);
+                currentKeyIndex = 0;
+            }
+        }
+
+        if (currentKeyIndex > 0) {
+            processingResult(
+                    matchingLegacyNative(
+                            nativePtr, keyBuffer, rollingStartIntervalNumberBuffer, currentKeyIndex),
+                    keys,
+                    result);
+        }
+        Log.log
+                .atInfo()
+                .log(
+                        "ExposureMatchingTracer.traceWithNative() %d keys matched, total %d keys",
+                        result.size(), keyCount);
+        return Pair.create(keyCount, result);
+    }
+
+    private static void processingResult(
+            int[] matchedIdIndexes, TemporaryExposureKey[] keys, Set<TemporaryExposureKey> result) {
+        if (matchedIdIndexes == null || matchedIdIndexes.length <= 0) {
+            return;
+        }
+        for (int matchedIdIndex : matchedIdIndexes) {
+            result.add(keys[matchedIdIndex]);
+        }
+    }
+
+    public int getLastProcessedKeyCount() {
+        return lastProcessedKeyCountNative(nativePtr);
+    }
+
+    @Override
+    public void close() {
+        releaseNative(nativePtr);
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingRequest.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingRequest.java
new file mode 100644
index 0000000..00b3f89
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingRequest.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.collect.ImmutableList;
+import com.google.samples.exposurenotification.nearby.ExposureConfiguration;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.data.fileformat.StreamingTemporaryExposureKeyFileParser;
+import com.google.samples.exposurenotification.storage.CloseableIterable;
+import com.google.samples.exposurenotification.storage.CloseableIterables;
+import com.google.samples.exposurenotification.storage.ExposureConfigurationConverter;
+import com.google.samples.exposurenotification.storage.ExposureConfigurationProto;
+import com.google.samples.exposurenotification.storage.MatchingRequestProto;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static com.google.samples.exposurenotification.Log.log;
+
+/**
+ * Encloses information about a matching request.
+ */
+public class MatchingRequest {
+    private final long requestId;
+    private final String shortenedPackageName;
+    private final MatchingRequestProto matchingRequestProto;
+
+    public MatchingRequest(long requestId, MatchingRequestProto matchingRequestProto) {
+        this.requestId = requestId;
+        this.matchingRequestProto = matchingRequestProto;
+        shortenedPackageName = createShortenedPackageName(matchingRequestProto.getPackageName());
+    }
+
+    /**
+     * The generated requestId.
+     */
+    public long requestId() {
+        return requestId;
+    }
+
+    /**
+     * Package name of client request.
+     */
+    public String packageName() {
+        return matchingRequestProto.getPackageName();
+    }
+
+    /**
+     * Gets the shortened version of the package name, used primarily for logging.
+     */
+    public String getShortenedPackageName() {
+        return shortenedPackageName;
+    }
+
+    /**
+     * Signature hash of client app.
+     */
+    public byte[] signatureHash() {
+        return matchingRequestProto.getSignatureHash().toByteArray();
+    }
+
+    /**
+     * Token associated with request.
+     */
+    public String token() {
+        return matchingRequestProto.getToken();
+    }
+
+    /**
+     * The SHA-256 has associated with the key files in this matching request, in base64.
+     */
+    public String keyFilesHash() {
+        return matchingRequestProto.getKeyFilesHash();
+    }
+
+    public long requestTimeMillis() {
+        return matchingRequestProto.getRequestTimeMillis();
+    }
+
+    /**
+     * exposure configuration for request.
+     */
+    public ExposureConfiguration exposureConfiguration() {
+        return new ExposureConfigurationConverter()
+                .reverse()
+                .convert(matchingRequestProto.getExposureConfiguration());
+    }
+
+    public ExposureConfigurationProto exposureConfigurationProto() {
+        return matchingRequestProto.getExposureConfiguration();
+    }
+
+    /**
+     * Gets the diagnosis keys provided with this request. The returned closable must be closed
+     * after use so that the underlying resources (files) can be closed correctly.
+     */
+    public CloseableIterable<TemporaryExposureKey> diagnosisKeys() {
+        List<CloseableIterable<TemporaryExposureKey>> keyIterables = new ArrayList<>();
+        for (String filePath : matchingRequestProto.getKeyFilesList()) {
+            try {
+                keyIterables.add(StreamingTemporaryExposureKeyFileParser.parse(new File(filePath)));
+            } catch (IOException e) {
+                log.atSevere().withCause(e).log("Can't parse keys from %s", filePath);
+            }
+        }
+        return CloseableIterables.concat(keyIterables);
+    }
+
+    public ImmutableList<String> diagnosisKeyFiles() {
+        return ImmutableList.copyOf(matchingRequestProto.getKeyFilesList());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(requestId, matchingRequestProto);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object object) {
+        if (object instanceof MatchingRequest) {
+            MatchingRequest that = (MatchingRequest) object;
+            return this.requestId == that.requestId
+                    && this.matchingRequestProto.equals(that.matchingRequestProto);
+        }
+        return false;
+    }
+
+    /**
+     * Shortens package name to the string past the second to last period.
+     */
+    static String createShortenedPackageName(String packageName) {
+        if (packageName.lastIndexOf(".") == -1) {
+            return packageName;
+        }
+        return packageName.substring(
+                packageName.lastIndexOf(".", packageName.lastIndexOf(".") - 1) + 1);
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/Period.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/Period.java
new file mode 100644
index 0000000..d0016b7
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/Period.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import com.google.auto.value.AutoValue;
+
+/** A pair of two scans. */
+@AutoValue
+abstract class Period {
+    static Period create(TimeAndAttenuation scan1, TimeAndAttenuation scan2) {
+        return new AutoValue_Period(scan1, scan2);
+    }
+
+    abstract TimeAndAttenuation scan1();
+
+    abstract TimeAndAttenuation scan2();
+
+    public int duration() {
+        return scan2().time() - scan1().time();
+    }
+
+    public int attenuationDiff() {
+        return scan2().attenuation() - scan1().attenuation();
+    }
+
+    public int calculateTimeCross(int threshold, boolean interpolate) {
+        int timeCross = scan1().time();
+        if (interpolate && attenuationDiff() != 0) {
+            // calculate the time at which the interpolated attenuation equals the threshold.
+            timeCross =
+                    (int)
+                            Math.round(
+                                    scan1().time()
+                                            + (threshold - scan1().attenuation())
+                                            / (double) attenuationDiff()
+                                            * duration());
+        }
+        return timeCross;
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ProvideDiagnosisKeys.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ProvideDiagnosisKeys.java
new file mode 100644
index 0000000..64893f7
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/ProvideDiagnosisKeys.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import android.content.Context;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.Lists;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TEKSignatureList;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipInputStream;
+
+/**
+ * This class is a heavily modified version of the original code which accepts a list of signed
+ * diagnosis key ZIP files that match the file format described at
+ * https://developers.google.com/android/exposure-notifications/exposure-key-file-format.
+ * <p>
+ * This code demonstrates how to call the required functions to verify the file format is
+ * correct and the keys have been properly signed.
+ * <p>
+ * The code has been greatly simplified from the original version, primarily in the way errors
+ * are handled.
+ */
+public class ProvideDiagnosisKeys {
+    private final Context context;
+
+    public ProvideDiagnosisKeys(Context context) {
+        this.context = context;
+    }
+
+    public boolean verify(String callingPackage, File signedKeyFile) {
+        KeyFileSignature keyFileSignature = unzip(context, signedKeyFile);
+        ArrayList<KeyFileSignature> signatures = Lists.newArrayList(keyFileSignature);
+
+        SignatureVerifier signatureVerifier = new SignatureVerifier();
+        try {
+            return signatureVerifier.verify(callingPackage, signatures);
+        } catch (Exception e) {
+            throw new RuntimeException("Verification failed", e);
+        }
+    }
+
+    /**
+     * Unzips a diagnosis keys zip file into an internal folder to allow for
+     * verification of the signature and processing of the keys by {@link MatchingJni}
+     * <p>
+     * FIXME: This code is NOT threadsafe and is incomplete as it exists. Key files are extracted
+     * to a folder and those keys + signatures are not cleaned up here (or anywhere else in the
+     * provided snippets). After matching occurs, these files should be removed by whatever
+     * system is running the key matching process.
+     *
+     * @param context           The context to use
+     * @param diagnosisKeysFile The zip file
+     * @return A {@link KeyFileSignature} that can be verified by {@link SignatureVerifier}
+     */
+    static KeyFileSignature unzip(Context context, File diagnosisKeysFile) {
+        File targetFolder = new File(context.getFilesDir(), "en_diagnosis_keys");
+        if (!targetFolder.mkdirs()) {
+            throw new RuntimeException("Can't create folder for keys");
+        }
+
+        ArrayList<KeyFileSignature> results = new ArrayList<>();
+        byte[] buffer = new byte[2048];
+        TEKSignatureList signatureList = null;
+        File keyFile = null;
+
+        try {
+            // We use a ZipInputStream, rather than a ZipFile, to minimize the memory footprint.
+            ZipInputStream zipStream = new ZipInputStream(new FileInputStream(diagnosisKeysFile));
+
+            ZipEntry entry;
+            while ((entry = zipStream.getNextEntry()) != null) {
+                String entryName = getValidatedName(entry);
+                if (entryName.equals(ContactTracingFeature.diagnosisKeySignatureFileName())) {
+                    if (signatureList != null) {
+                        deleteFile(targetFolder);
+                        throw new IllegalStateException("Archive contains multiple " + entryName);
+                    }
+                    signatureList =
+                            TEKSignatureList.parseFrom(zipStream);
+                } else if (entryName.equals(ContactTracingFeature.diagnosisKeyBinFileName())) {
+                    if (keyFile != null) {
+                        deleteFile(targetFolder);
+                        throw new IllegalStateException("Archive contains multiple " + entryName);
+                    }
+                    keyFile = new File(targetFolder, "export.bin");
+
+                    if (!keyFile.createNewFile()) {
+                        deleteFile(targetFolder);
+                        throw new RuntimeException(
+                                "Can't create file " + keyFile.getAbsolutePath());
+                    }
+                    try (FileOutputStream output = new FileOutputStream(keyFile.getAbsolutePath())) {
+                        int len;
+                        while ((len = zipStream.read(buffer)) > 0) {
+                            output.write(buffer, 0, len);
+                        }
+                    }
+                } else {
+                    deleteFile(targetFolder);
+                    throw new IllegalStateException("Invalid key file entry " + entryName);
+                }
+            }
+        } catch (IOException e) {
+            deleteFile(targetFolder);
+            throw new RuntimeException("Failed to extract diagnosis keys from " +
+                    diagnosisKeysFile.getAbsolutePath(), e);
+        }
+
+        if (keyFile == null || signatureList == null) {
+            deleteFile(targetFolder);
+            throw new RuntimeException("Invalid file content: " +
+                    diagnosisKeysFile.getAbsolutePath());
+        }
+        return KeyFileSignature.create(keyFile, signatureList);
+    }
+
+    /**
+     * Return the name of a ZipEntry after attempting to verifying that it does not exploit any
+     * path traversal attacks.
+     *
+     * @throws ZipException if {@param zipEntry} contains any possible path traversal
+     *                      characters.
+     */
+    private static String getValidatedName(ZipEntry zipEntry) throws ZipException {
+        String name = zipEntry.getName();
+        if (name.contains("..")) {
+            // If the string does contain "..", break it down into its actual name
+            // elements to ensure it actually contains ".." as a name, not just a
+            // name like "foo..bar" or even "foo..", which should be fine.
+            File file = new File(name);
+            while (file != null) {
+                if (file.getName().equals("..")) {
+                    throw new ZipException("Illegal name: " + name);
+                }
+                file = file.getParentFile();
+            }
+        }
+        return name;
+    }
+
+    private static boolean deleteFile(File file) {
+        if (file.isDirectory()) {
+            File[] children = file.listFiles();
+            if (children != null) {
+                for (File child : children) {
+                    deleteFile(child);
+                }
+            }
+        }
+
+        return file.delete();
+    }
+
+    @AutoValue
+    abstract static class KeyFileSignature implements SignatureVerifier.KeyExport {
+        static KeyFileSignature create(File keyFile, TEKSignatureList signatureList) {
+            return new AutoValue_ProvideDiagnosisKeys_KeyFileSignature(keyFile, signatureList);
+        }
+
+        /**
+         * The file of the diagnosis keys.
+         */
+        abstract File keyFile();
+
+        /**
+         * The signature list that was read from export.sig file.
+         */
+        @Override
+        public abstract TEKSignatureList signatureList();
+
+        @Override
+        public InputStream openKeyInputStream() throws IOException {
+            return new FileInputStream(keyFile());
+        }
+    }
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/RiskScoreCalculator.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/RiskScoreCalculator.java
new file mode 100644
index 0000000..aa19226
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/RiskScoreCalculator.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.samples.Clock;
+import com.google.samples.Clock.DefaultClock;
+import com.google.samples.exposurenotification.nearby.ExposureConfiguration;
+import com.google.samples.exposurenotification.nearby.RiskLevel;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+import com.google.samples.exposurenotification.storage.ExposureRecord;
+
+import java.util.List;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * Used to calculate risk score based on client configuration and exposures.
+ */
+public class RiskScoreCalculator {
+    private final Clock clock;
+
+    public static RiskScoreCalculator create() {
+        return new RiskScoreCalculator(DefaultClock.getInstance());
+    }
+
+    public static RiskScoreCalculator create(Clock clock) {
+        return new RiskScoreCalculator(clock);
+    }
+
+    @VisibleForTesting
+    RiskScoreCalculator(Clock clock) {
+        this.clock = clock;
+    }
+
+    /**
+     * Returns a V1 risk score. Returns 0 if calculated risk score is below {@link
+     * ExposureConfiguration#getMinimumRiskScore()}. Throws IllegalArgumentException on invalid input.
+     */
+    public int calculateRiskScore(
+            TemporaryExposureKey diagnosisKey,
+            ExposureRecord exposureRecord,
+            long dateMillisSinceEpoch,
+            ExposureConfiguration configuration) {
+
+        int attenuationScore =
+                configuration
+                        .getAttenuationScores()[bucketAttenuationValue(exposureRecord.getAttenuationValue())];
+        int daysSinceLastExposureScore =
+                configuration.getDaysSinceLastExposureScores()[bucketLatencyDays(dateMillisSinceEpoch)];
+        int durationScore =
+                configuration.getDurationScores()[bucketDuration(exposureRecord.getDurationSeconds())];
+        int transmissionRiskScore =
+                diagnosisKey.getTransmissionRiskLevel() == RiskLevel.RISK_LEVEL_INVALID
+                        ? 1
+                        : configuration.getTransmissionRiskScores()[bucketRiskLevel(diagnosisKey)];
+        int riskScore =
+                attenuationScore * daysSinceLastExposureScore * durationScore * transmissionRiskScore;
+
+        if (riskScore >= configuration.getMinimumRiskScore()) {
+            return riskScore;
+        } else {
+            return 0;
+        }
+    }
+
+    public static int getAttenuationScore(
+            int weightedAttenuation, ExposureConfiguration configuration) {
+        return configuration.getAttenuationScores()[bucketAttenuationValue(weightedAttenuation)];
+    }
+
+    private int bucketRiskLevel(TemporaryExposureKey diagnosisKey) {
+        // Enum of risk levels starts at 1, with 0 reserved for RISK_LEVEL_INVALID.
+        return diagnosisKey.getTransmissionRiskLevel() - 1;
+    }
+
+    private static int bucketAttenuationValue(int attenuationValue) {
+        List<Integer> bucketBounds =
+                ContactTracingFeature.riskScoreAttenuationValueBuckets();
+        for (int bucket = 0; bucket < bucketBounds.size(); bucket++) {
+            if (attenuationValue > bucketBounds.get(bucket)) {
+                return bucket;
+            }
+        }
+        return bucketBounds.size();
+    }
+
+    private int bucketLatencyDays(long dateMillisSinceEpoch) {
+        List<Integer> bucketBounds =
+                ContactTracingFeature.riskScoreLatencyDaysBuckets();
+        long timeNow = clock.currentTimeMillis();
+        long latencyDays = MILLISECONDS.toDays(timeNow - dateMillisSinceEpoch);
+        for (int bucket = 0; bucket < bucketBounds.size(); bucket++) {
+            if (latencyDays >= bucketBounds.get(bucket)) {
+                return bucket;
+            }
+        }
+        return bucketBounds.size();
+    }
+
+    private int bucketDuration(long durationSeconds) {
+        List<Integer> bucketBounds = ContactTracingFeature.riskScoreDurationBuckets();
+        long durationMinutes = SECONDS.toMinutes(durationSeconds);
+        for (int bucket = 0; bucket < bucketBounds.size(); bucket++) {
+            if (durationMinutes <= bucketBounds.get(bucket)) {
+                return bucket;
+            }
+        }
+        return bucketBounds.size();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/SightingRecordWithMetadata.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/SightingRecordWithMetadata.java
new file mode 100644
index 0000000..6ace739
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/SightingRecordWithMetadata.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import com.google.auto.value.AutoValue;
+import com.google.samples.exposurenotification.data.BluetoothMetadata;
+import com.google.samples.exposurenotification.storage.SightingRecord;
+
+/** A pair of {@link SightingRecord} and {@link BluetoothMetadata}. */
+@AutoValue
+abstract class SightingRecordWithMetadata {
+    static SightingRecordWithMetadata create(
+            SightingRecord sightingRecord, BluetoothMetadata metadata) {
+        return new AutoValue_SightingRecordWithMetadata(sightingRecord, metadata);
+    }
+
+    abstract SightingRecord sightingRecord();
+
+    /**
+     * A decrypted version of {@link SightingRecord#getAssociatedEncryptedMetadata()} of {@link
+     * #sightingRecord()}.
+     */
+    abstract BluetoothMetadata metadata();
+
+    public int attenuationValue() {
+        return Math.max(metadata().txPower() - sightingRecord().getRssi(), 0);
+    }
+
+    public int secondsSinceLastScan(int maxMinutesSinceLastScan, int defaultMinutesSinceLastScan) {
+        // Default value, indicating we do not know when was the previous scan.
+        if (sightingRecord().getPreviousScanEpochSeconds() == 0) {
+            return (int) MINUTES.toSeconds(defaultMinutesSinceLastScan);
+        }
+
+        return (int)
+                MINUTES.toSeconds(
+                        Math.min(
+                                maxMinutesSinceLastScan,
+                                SECONDS.toMinutes(sightingRecord().getEpochSeconds())
+                                        - SECONDS.toMinutes(sightingRecord().getPreviousScanEpochSeconds())));
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/SignatureVerifier.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/SignatureVerifier.java
new file mode 100644
index 0000000..9a24446
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/SignatureVerifier.java
@@ -0,0 +1,662 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Base64;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Splitter.MapSplitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.SignatureInfo;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TEKSignature;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TEKSignatureList;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKeyExport;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+import com.google.samples.exposurenotification.matching.ProvideDiagnosisKeys.KeyFileSignature;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * Verified signatures of diagnosis key files, given a raw input stream to the file, and a metadata
+ * proto object.
+ */
+public class SignatureVerifier {
+
+    /**
+     * The key export format for verification purpose.
+     */
+    public interface KeyExport {
+
+        /**
+         * The signature list that was read from export.sig file.
+         */
+        TEKSignatureList signatureList();
+
+        /**
+         * Opens input stream for diagnosis key file. Caller of this method is responsible for closing
+         * it.
+         */
+        InputStream openKeyInputStream() throws IOException;
+    }
+
+    public static final String KEY_VERSION_JOINER = "-";
+
+    // map of supported signature algorithms - add as necessary
+    private static final ImmutableMap<String, String> OID_TO_NAME_MAP =
+            ImmutableMap.of(
+                    "1.2.840.10045.4.3.2", "SHA256withECDSA",
+                    "1.2.840.10045.4.3.4", "SHA512withECDSA");
+
+    private static final int STREAMING_BYTE_BUFFER_SIZE = 4096;
+
+    @Nullable
+    private byte[] keyFilesHash;
+    @Nullable
+    private String debugPublicKey;
+
+    /**
+     * Performs verification that the key exports (really {@link KeyFileSignature})s provided
+     * have been properly signed by the PHA key that matches the app's package. It also
+     * performs matching for co-signatures in order to support keys signed by multiple PHAs.
+     *
+     * @param callingPackage The calling package -- that is, the package name of the health
+     *                       authority app calling this code.
+     * @param exports        List of KeyFileSignature extracted by
+     *                       {@link ProvideDiagnosisKeys#unzip(Context, File)}
+     * @return {@code true} if the files were properly signed, {@code false} otherwise
+     */
+    public boolean verify(String callingPackage, List<? extends KeyExport> exports)
+            throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException,
+            SignatureException, ExecutionException, InterruptedException {
+        Preconditions.checkArgument(!exports.isEmpty());
+
+        // all chunks in batch must be present to verify
+        BitSet batchChunks = new BitSet(exports.size());
+
+        // b/157995134 would enable multiple whole batches and thus would need a map to verify.
+        Map<TemporaryExposureKeyExport, BitSet> batchChunkMap = new HashMap<>();
+
+        // all timestamps in a batch should match
+        long startTime = Long.MIN_VALUE;
+        long endTime = Long.MIN_VALUE;
+
+        // retrieve public keys - same keys will be used throughout
+        PartnerPublicKeySets partnerKeySet = getKeysForPackage(callingPackage, debugPublicKey);
+        if (partnerKeySet.cosignSets.isEmpty()) {
+            throw new SignatureException("no public key found for package " + callingPackage);
+        }
+
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        MessageDigest keyHash = MessageDigest.getInstance("SHA-256");
+
+        for (KeyExport export : exports) {
+            TEKSignatureList tekSignatures = export.signatureList();
+            if (tekSignatures.getSignaturesCount() == 0) {
+                throw new SignatureException("data is unsigned");
+            }
+
+            // create list of verifications for each rotation. only one verification within the rotation
+            // has to verify successfully. within each cosigned set, all keys must verify successfully for
+            // the set to verify.
+            List<Verification> setVerifications = new ArrayList<>(partnerKeySet.cosignSets.size());
+            for (CosignSet cosignSet : partnerKeySet.cosignSets) {
+                setVerifications.add(createCosignatureVerifications(tekSignatures, cosignSet));
+            }
+
+            // read bytes, and simultaneously update all signatures and divert them to another thread
+            // where we will be reconstructing the metadata from those bytes
+            TemporaryExposureKeyExport keyMetadata;
+            try (PipedInputStream in = new PipedInputStream(STREAMING_BYTE_BUFFER_SIZE)) {
+                Future<TemporaryExposureKeyExport> protoDecodeFuture;
+                try (PipedOutputStream out = new PipedOutputStream(in);
+                     InputStream is = export.openKeyInputStream()) {
+                    protoDecodeFuture =
+                            executor.submit(() -> TemporaryExposureKeyFileMetadataParser.parse(in));
+
+                    // read stream and divert to secondary pipe and every signature
+                    byte[] buffer = new byte[STREAMING_BYTE_BUFFER_SIZE];
+                    int len = is.read(buffer);
+                    while (len != -1) {
+                        out.write(buffer, 0, len);
+                        if (ContactTracingFeature.calculateDiagnosisKeyHash()) {
+                            keyHash.update(buffer, 0, len);
+                        }
+                        for (Verification verification : setVerifications) {
+                            verification.update(buffer, 0, len);
+                        }
+                        len = is.read(buffer);
+                    }
+                }
+
+                // finalize signature verification - only one set needs to match
+                boolean matched = false;
+                for (Verification verification : setVerifications) {
+                    if (verification.verify()) {
+                        matched = true;
+                        break;
+                    }
+                }
+                if (!matched) {
+                    Log.log.atWarning().log("The necessary signatures did not verify");
+                    return false;
+                }
+
+                // retrieve metadata generated on other thread, and only now can we close the input stream
+                keyMetadata = protoDecodeFuture.get();
+            }
+
+            if (keyMetadata.getBatchSize() > ContactTracingFeature.signatureMaxAllowedBatchGroupSize()) {
+                throw new UnsupportedOperationException(
+                        "Signature batch size is "
+                                + keyMetadata.getBatchSize()
+                                + ". It must not be greater than "
+                                + ContactTracingFeature.signatureMaxAllowedBatchGroupSize()
+                                + " for "
+                                + createExposureKeyString(keyMetadata));
+            }
+
+            // verify start/end times...
+            // When enableMultipleWholeBatchSignatureVerification is enabled, this is always verified as
+            // start and end timestamps are present in the key.
+            if (!ContactTracingFeature.enableMultipleWholeBatchSignatureVerification()) {
+                if (startTime == Long.MIN_VALUE && endTime == Long.MIN_VALUE) {
+                    startTime = keyMetadata.getStartTimestamp();
+                    endTime = keyMetadata.getEndTimestamp();
+                } else {
+                    if (keyMetadata.getStartTimestamp() != startTime
+                            || keyMetadata.getEndTimestamp() != endTime) {
+                        throw new IOException(
+                                "exposure key start/end times ("
+                                        + keyMetadata.getStartTimestamp()
+                                        + "/"
+                                        + keyMetadata.getEndTimestamp()
+                                        + ") do not match ("
+                                        + startTime
+                                        + "/"
+                                        + endTime
+                                        + ") for "
+                                        + createExposureKeyString(keyMetadata));
+                    }
+                }
+            }
+
+            // verify metadata signature count...
+            if (tekSignatures.getSignaturesCount() != keyMetadata.getSignatureInfosCount()) {
+                throw new SignatureException(
+                        "expected "
+                                + keyMetadata.getSignatureInfosCount()
+                                + " signatures, but found "
+                                + tekSignatures.getSignaturesCount()
+                                + " for "
+                                + createExposureKeyString(keyMetadata));
+            }
+
+            // key for collecting all files belonging to the same batch
+            TemporaryExposureKeyExport keyMetadataForMap =
+                    keyMetadata.toBuilder().clearBatchNum().build();
+
+            // verify batches, if fields are set
+            if (keyMetadata.hasBatchSize()) {
+                if (!keyMetadata.hasBatchNum()) {
+                    throw new SignatureException(
+                            "key metadata specifies batch size but not batch number for "
+                                    + createExposureKeyString(keyMetadata));
+                }
+
+                // batch number is ordinal 1 based
+                if (keyMetadata.getBatchNum() < 1) {
+                    throw new SignatureException(
+                            "batch number should be one or greater for " + createExposureKeyString(keyMetadata));
+                }
+
+                if (keyMetadata.getBatchNum() > keyMetadata.getBatchSize()) {
+                    throw new SignatureException(
+                            "batch number should not be greater than batch size for "
+                                    + createExposureKeyString(keyMetadata));
+                }
+                if (ContactTracingFeature.enableMultipleWholeBatchSignatureVerification()) {
+                    if (!batchChunkMap.containsKey(keyMetadataForMap)) {
+                        batchChunkMap.put(keyMetadataForMap, new BitSet(keyMetadata.getBatchSize()));
+                    }
+                    // batch number is ordinal 1 based
+                    batchChunkMap.get(keyMetadataForMap).set(keyMetadata.getBatchNum() - 1);
+
+                } else {
+                    if (keyMetadata.getBatchSize() != exports.size()) {
+                        throw new SignatureException(
+                                "signature batch size ("
+                                        + keyMetadata.getBatchSize()
+                                        + " does not match actual batch size ("
+                                        + exports.size()
+                                        + ") for "
+                                        + createExposureKeyString(keyMetadata));
+                    }
+                    batchChunks.set(keyMetadata.getBatchNum() - 1);
+                }
+
+            } else {
+                if (ContactTracingFeature.enableMultipleWholeBatchSignatureVerification()) {
+                    if (!batchChunkMap.containsKey(keyMetadataForMap)) {
+                        batchChunkMap.put(keyMetadataForMap, new BitSet(keyMetadata.getBatchSize()));
+                    }
+                    batchChunkMap.get(keyMetadataForMap).set(0);
+                } else {
+                    batchChunks.set(0);
+                }
+            }
+        }
+
+        // verify all chunks seen
+        if (!ContactTracingFeature.enableMultipleWholeBatchSignatureVerification()
+                && batchChunks.nextClearBit(0) < exports.size()) {
+            throw new IOException(
+                    "missing some chunks out of " + exports.size() + " chunks expected in batch");
+        } else {
+            SignatureException exceptions = null;
+            for (Map.Entry<TemporaryExposureKeyExport, BitSet> entry : batchChunkMap.entrySet()) {
+                TemporaryExposureKeyExport keyMetaData = entry.getKey();
+                if (keyMetaData.hasBatchSize()
+                        && keyMetaData.getBatchSize() != entry.getValue().cardinality()) {
+                    SignatureException e =
+                            new SignatureException(
+                                    "Signature batch size ("
+                                            + keyMetaData.getBatchSize()
+                                            + " does not match actual batch size ("
+                                            + entry.getValue().cardinality()
+                                            + ") for batch: "
+                                            + createExposureKeyString(keyMetaData));
+                    if (exceptions == null) {
+                        exceptions = e;
+                    } else {
+                        exceptions.addSuppressed(e);
+                    }
+                }
+            }
+            if (exceptions != null) {
+                throw exceptions;
+            }
+        }
+
+        if (ContactTracingFeature.calculateDiagnosisKeyHash()) {
+            keyFilesHash = keyHash.digest();
+        }
+        Log.log.atInfo().log("Signature verification succeeded");
+        return true;
+    }
+
+    /**
+     * Gets the SHA-256 hash of the key files. If not available, returns an empty array.
+     */
+    public byte[] getKeyFilesHash() {
+        if (keyFilesHash == null) {
+            return new byte[0];
+        }
+        return keyFilesHash;
+    }
+
+    public void setDebugPublicKey(@Nullable String debugPublicKey) {
+        this.debugPublicKey = debugPublicKey;
+    }
+
+    private static final Splitter PUBLIC_KEY_SPLITTER = Splitter.on(':').limit(2);
+    private static final Splitter ROTATION_LIST_SPLITTER = Splitter.on('|');
+
+    private static final MapSplitter COSIGNING_LIST_SPLITTER =
+            Splitter.on('&').withKeyValueSeparator(',');
+
+    // returns a list of potential keysets where only one keyset within the list must verify - this
+    // supports key rotation. within a keyset, all keys within the set must verify - this support
+    // cosigning
+    private static PartnerPublicKeySets getKeysForPackage(
+            String packageName, @Nullable String debugPublicKey)
+            throws NoSuchAlgorithmException, InvalidKeySpecException {
+        // This is the list of all the flag-configured public signing keys, one element for each
+        // partner's keys.
+        List<String> publicKeyList = ContactTracingFeature.partnerPublicKeys();
+        Log.log
+                .atFine()
+                .log("getKeysForPackage: package name: %s, debug: %s, partner public keys flag: %s",
+                        packageName, debugPublicKey, TextUtils.join("\n", publicKeyList));
+
+        // Start with an empty set of keys.
+        PartnerPublicKeySets partnerKeySets = new PartnerPublicKeySets(packageName);
+
+        // Parse the flag-defined keys.
+        for (String publicKeyInfo : publicKeyList) {
+            PartnerPublicKeySets candidateKeySet = parsePublicKeyFlag(publicKeyInfo);
+            if (candidateKeySet.packageName.equals(packageName)) {
+                Log.log
+                        .atInfo()
+                        .log("Found flag-defined keys %s for package %s", publicKeyInfo, packageName);
+                partnerKeySets.cosignSets.addAll(candidateKeySet.cosignSets);
+            }
+        }
+
+        // One "extra" public key definition can be set via debug settings. If it is set, and its
+        // package is the one we're working with here, we add it as an additional cosign set so that
+        // it has an "additive" effect. That is: if the given keyfile's signature passes the debug
+        // key's verification, we accept the file, otherwise we continue evaluating the flag-defined
+        // public keys.
+        if (debugPublicKey != null) {
+            Log.log.atInfo().log("Debug key set: %s", debugPublicKey);
+            try {
+                PartnerPublicKeySets debugKeySet = parsePublicKeyFlag(debugPublicKey);
+                if (debugKeySet.packageName.equals(packageName)) {
+                    Log.log.atInfo().log("Adding debug key set to partner's sets: %s", packageName);
+                    // If there are no flag-defined keys for the package name we're interested, that's OK.
+                    // partnerKeySet will be an empty set here, which is fine; we add the debug keys and
+                    // continue.
+                    partnerKeySets.cosignSets.addAll(debugKeySet.cosignSets);
+                }
+            } catch (InvalidKeySpecException e) {
+                Log.log
+                        .atWarning()
+                        .withCause(e)
+                        .log("Custom diagnosis key signature fields of debug mode is invalid and " +
+                                "will be ignored! ");
+            }
+        }
+
+        return partnerKeySets;
+    }
+
+    /**
+     * Parse the public key flag value for one partner, e.g.
+     * "com.my.package:keyid-v1,<keybytes>&otherid-v1,<otherbytes>|keyid-v2,<morebytes>"
+     */
+    private static PartnerPublicKeySets parsePublicKeyFlag(String flagValue)
+            throws NoSuchAlgorithmException, InvalidKeySpecException {
+        List<String> parts = PUBLIC_KEY_SPLITTER.splitToList(flagValue);
+        String partnerPackageName = parts.get(0);
+        PartnerPublicKeySets partnerKeys = new PartnerPublicKeySets(partnerPackageName);
+
+        List<String> partnerKeyConfigs = ROTATION_LIST_SPLITTER.splitToList(parts.get(1));
+        for (String cosignSetConfig : partnerKeyConfigs) {
+            // Split each cosign config into keys mapped by their name.
+            Map<String, String> cosignKeys = COSIGNING_LIST_SPLITTER.split(cosignSetConfig);
+            CosignSet cosignSet = new CosignSet();
+            for (Map.Entry<String, String> cosignKeyEntry : cosignKeys.entrySet()) {
+                byte[] publicKeyBytes = Base64.decode(cosignKeyEntry.getValue(), Base64.DEFAULT);
+                cosignSet.keys.put(
+                        cosignKeyEntry.getKey(),
+                        KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(publicKeyBytes)));
+            }
+            partnerKeys.cosignSets.add(cosignSet);
+        }
+
+        return partnerKeys;
+    }
+
+    private static Verification createCosignatureVerifications(
+            TEKSignatureList tekSignatures, CosignSet cosignSet)
+            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+        Preconditions.checkArgument(tekSignatures.getSignaturesCount() > 0);
+        Preconditions.checkArgument(!cosignSet.keys.isEmpty());
+
+        // if there are more public keys than signatures in the set, the signatures cannot verify
+        if (tekSignatures.getSignaturesCount() < cosignSet.keys.size()) {
+            return new FailedVerification();
+        }
+
+        // enforce that all public keys are matched
+        HashSet<String> matchedPublicKeyNames = new HashSet<>();
+
+        // for each signature...
+        ImmutableList.Builder<Verification> cosignatureVerificationsBuilder =
+                ImmutableList.builderWithExpectedSize(tekSignatures.getSignaturesCount());
+        for (TEKSignature tekSignature : tekSignatures.getSignaturesList()) {
+            SignatureInfo info = tekSignature.getSignatureInfo();
+
+            // get correct public key to match
+            String publicKeyName;
+            PublicKey publicKey = null;
+            if (info.hasVerificationKeyId()) {
+                publicKeyName =
+                        info.getVerificationKeyId() + KEY_VERSION_JOINER + info.getVerificationKeyVersion();
+                if (cosignSet.keys.containsKey(publicKeyName)) {
+                    publicKey = cosignSet.keys.get(publicKeyName);
+                }
+            } else {
+                if (tekSignatures.getSignaturesCount() > 1) {
+                    throw new SignatureException(
+                            "signature in cosigning group does not specify verification_key_id");
+                }
+                if (ContactTracingFeature.ignoreUnmatchedSignatures()) {
+                    if (cosignSet.keys.size() > 1) {
+                        return new FailedVerification();
+                    }
+                } else {
+                    Preconditions.checkState(cosignSet.keys.size() == 1);
+                }
+                Map.Entry<String, PublicKey> entry = cosignSet.keys.entrySet().iterator().next();
+                publicKeyName = entry.getKey();
+                publicKey = entry.getValue();
+            }
+
+            // if no matching public key found, move on to the next key, otherwise mark key as seen
+            if (publicKey == null) {
+                if (ContactTracingFeature.ignoreUnmatchedSignatures()) {
+                    continue;
+                } else {
+                    Log.log.atWarning().log("a verification failed due to an unmatched signature (strict)");
+                    // if one key in the set cannot be matched, the entire set will fail anyways
+                    return new FailedVerification();
+                }
+            } else if (ContactTracingFeature.ignoreUnmatchedSignatures()) {
+                matchedPublicKeyNames.add(publicKeyName);
+            }
+
+            // create verifier for public key and add to the list
+            String signatureAlgorithmName = OID_TO_NAME_MAP.get(info.getSignatureAlgorithm());
+            if (signatureAlgorithmName == null) {
+                throw new NoSuchAlgorithmException(
+                        "unsupported signature OID: " + info.getSignatureAlgorithm());
+            }
+            Signature signatureAttempt = Signature.getInstance(signatureAlgorithmName);
+            signatureAttempt.initVerify(publicKey);
+            cosignatureVerificationsBuilder.add(
+                    new SignatureVerification(signatureAttempt, tekSignature.getSignature().toByteArray()));
+        }
+
+        // check all public keys are being verified with none left out
+        ImmutableList<Verification> verifications = cosignatureVerificationsBuilder.build();
+        if (verifications.size() < cosignSet.keys.size()) {
+            if (ContactTracingFeature.ignoreUnmatchedSignatures()) {
+                Log.log
+                        .atInfo()
+                        .log(
+                                "More public keys in this cosign set than signatures/verifications in this keyfile"
+                                        + " (%d > %d). Continuing with other cosign sets.",
+                                cosignSet.keys.size(), verifications.size());
+                // Failing this cosign set allows us to continue with other cosign sets.
+                return new FailedVerification();
+            } else {
+                // Throwing an exception stops us from attempting signature verification with other cosign
+                // sets.
+                throw new SignatureException(
+                        "expected at least "
+                                + cosignSet.keys.size()
+                                + " signatures with matching ids, found only "
+                                + verifications.size());
+            }
+        }
+        if (ContactTracingFeature.ignoreUnmatchedSignatures()
+                && matchedPublicKeyNames.size() != cosignSet.keys.size()) {
+            Log.log
+                    .atInfo()
+                    .log(
+                            "Unequal number of signatures/verifications in this keyfile vs number of public keys"
+                                    + " in this cosign set. %d != %d. Continuing with other cosign sets.",
+                            matchedPublicKeyNames.size(), cosignSet.keys.size());
+            // Failing this cosign set (as opposed to throwing an exception) allows us to continue with
+            // other cosign sets.
+            return new FailedVerification();
+        }
+
+        return new CosignatureVerification(verifications);
+    }
+
+    private static String createExposureKeyString(TemporaryExposureKeyExport keyMetadata) {
+        return "[StartTimestamp="
+                + keyMetadata.getStartTimestamp()
+                + "; EndTimestamp="
+                + keyMetadata.getEndTimestamp()
+                + "; Region="
+                + keyMetadata.getRegion()
+                + "; BatchNum="
+                + keyMetadata.getBatchNum()
+                + "; BatchSize="
+                + keyMetadata.getBatchSize()
+                + "]";
+    }
+
+    private interface Verification {
+
+        void update(byte[] data, int off, int len) throws SignatureException;
+
+        boolean verify() throws SignatureException;
+    }
+
+    /**
+     * Individual verification that will immediately fail. Not necessarily indicative that all
+     * signature verifications for a file have failed.
+     */
+    private static class FailedVerification implements Verification {
+
+        @Override
+        public void update(byte[] data, int off, int len) {
+        }
+
+        @Override
+        public boolean verify() {
+            return false;
+        }
+    }
+
+    private static class CosignatureVerification implements Verification {
+
+        private final ImmutableList<Verification> cosignatures;
+
+        private CosignatureVerification(List<Verification> cosignatures) {
+            Preconditions.checkArgument(!cosignatures.isEmpty());
+            this.cosignatures = ImmutableList.copyOf(cosignatures);
+        }
+
+        @Override
+        public void update(byte[] data, int off, int len) throws SignatureException {
+            for (Verification verification : cosignatures) {
+                verification.update(data, off, len);
+            }
+        }
+
+        @Override
+        public boolean verify() throws SignatureException {
+            for (Verification verification : cosignatures) {
+                if (!verification.verify()) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+    }
+
+    private static class SignatureVerification implements Verification {
+
+        private final Signature signature;
+        private final byte[] comparison;
+
+        private SignatureVerification(Signature signature, byte[] comparison) {
+            this.signature = signature;
+            this.comparison = comparison;
+        }
+
+        @Override
+        public void update(byte[] data, int off, int len) throws SignatureException {
+            signature.update(data, off, len);
+        }
+
+        @Override
+        public boolean verify() throws SignatureException {
+            return signature.verify(comparison);
+        }
+    }
+
+    /**
+     * Value class to hold all the public signing keys for one partner's keyfiles.
+     *
+     * <p>The partner is identified by their app package name, and may have multiple possible sets of
+     * keys, called cosign sets. Among a given partner's cosign sets, at least one must successfully
+     * verify a keyfile's signature for the keyfile to pass.
+     *
+     * <p>It is supported for there to be more than one flag value with the same package name, in
+     * which case we will concat the lists of CosignSets for each flag so that if any of them pass,
+     * the keyfile passes.
+     *
+     * <p>This is additionally true for public keys supplied via the debug settings: If one is
+     * supplied for the same package as flag-defined keys, we'll add the debug key to the list of
+     * CosignSets and accept keyfiles which pass its verification.
+     */
+    private static class PartnerPublicKeySets {
+
+        private final String packageName;
+        private final List<CosignSet> cosignSets = new ArrayList<>();
+
+        private PartnerPublicKeySets(String packageName) {
+            this.packageName = packageName;
+        }
+    }
+
+    /**
+     * A set of keys that together must all successfully verify a keyfile's signature for the keyfile
+     * to pass.
+     */
+    private static class CosignSet {
+
+        private final Map<String, PublicKey> keys = new HashMap<>();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TemporaryExposureKeyExportV1Header.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TemporaryExposureKeyExportV1Header.java
new file mode 100644
index 0000000..4d94f03
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TemporaryExposureKeyExportV1Header.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import com.google.samples.ByteArrayValue;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/** Encloses constant values related file format definition. */
+public final class TemporaryExposureKeyExportV1Header {
+    // See https://developers.google.com/android/exposure-notifications/exposure-key-file-format
+    public static final ByteArrayValue HEADER =
+            ByteArrayValue.from("EK Export v1    ".getBytes(UTF_8)); // right padded with whitespaces
+
+    public static void readAndVerifyHeader(InputStream inputStream) throws IOException {
+        byte[] header = new byte[HEADER.length];
+        int totalBytesRead = 0;
+        if (ContactTracingFeature.readFullMetadataHeader()) {
+            int bytesRead = 0;
+            while (totalBytesRead < HEADER.length && bytesRead != -1) {
+                bytesRead = inputStream.read(header, totalBytesRead, HEADER.length - totalBytesRead);
+                totalBytesRead += bytesRead;
+            }
+        } else {
+            totalBytesRead = inputStream.read(header);
+        }
+        if (totalBytesRead != HEADER.length) {
+            throw new IOException(
+                    "Invalid header length (read "
+                            + totalBytesRead
+                            + " bytes, expected "
+                            + HEADER.length
+                            + " bytes)");
+        }
+        if (!Arrays.equals(header, HEADER.get())) {
+            throw new IOException("Invalid header: " + Arrays.toString(header));
+        }
+    }
+
+    private TemporaryExposureKeyExportV1Header() {}
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TemporaryExposureKeyFileMetadataParser.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TemporaryExposureKeyFileMetadataParser.java
new file mode 100644
index 0000000..440b9dc
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TemporaryExposureKeyFileMetadataParser.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.ExtensionRegistryLite;
+import com.google.protobuf.WireFormat;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.SignatureInfo;
+import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKeyExport;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A parser class to parse metadata from exposure key export file.
+ */
+public class TemporaryExposureKeyFileMetadataParser {
+    // An early returning option, should be disabled unless the correctness and robustness is proved
+    // when enabled.
+    private static final boolean EARLY_RETURN_ENABLED = false;
+
+    /**
+     * Parses the metadata (all the fields of {@link TemporaryExposureKeyExport} except the keys). It
+     * provides a memory efficient way to get the metadata.
+     */
+    public static TemporaryExposureKeyExport parse(File file) throws IOException {
+        return parse(new FileInputStream(file));
+    }
+
+    /**
+     * See {@link #parse(File)}
+     */
+    public static TemporaryExposureKeyExport parse(InputStream inputStream) throws IOException {
+        TemporaryExposureKeyExportV1Header.readAndVerifyHeader(inputStream);
+
+        CodedInputStream codedStream = CodedInputStream.newInstance(inputStream);
+        TemporaryExposureKeyExport.Builder builder = TemporaryExposureKeyExport.newBuilder();
+        int nextTag = codedStream.readTag();
+        boolean hasSeenKey = false;
+        while (nextTag != 0) {
+            switch (WireFormat.getTagFieldNumber(nextTag)) {
+                case TemporaryExposureKeyExport.START_TIMESTAMP_FIELD_NUMBER:
+                    builder.setStartTimestamp(codedStream.readFixed64());
+                    break;
+                case TemporaryExposureKeyExport.END_TIMESTAMP_FIELD_NUMBER:
+                    builder.setEndTimestamp(codedStream.readFixed64());
+                    break;
+                case TemporaryExposureKeyExport.REGION_FIELD_NUMBER:
+                    builder.setRegion(codedStream.readString());
+                    break;
+                case TemporaryExposureKeyExport.BATCH_NUM_FIELD_NUMBER:
+                    builder.setBatchNum(codedStream.readInt32());
+                    break;
+                case TemporaryExposureKeyExport.BATCH_SIZE_FIELD_NUMBER:
+                    builder.setBatchSize(codedStream.readInt32());
+                    break;
+                case TemporaryExposureKeyExport.SIGNATURE_INFOS_FIELD_NUMBER:
+                    SignatureInfo.Builder signatureBuilder = SignatureInfo.newBuilder();
+                    codedStream.readMessage(signatureBuilder, ExtensionRegistryLite.getEmptyRegistry());
+                    builder.addSignatureInfos(signatureBuilder);
+                    break;
+                case TemporaryExposureKeyExport.KEYS_FIELD_NUMBER:
+                    hasSeenKey = true;
+                    // fall through to skip keys.
+                default:
+                    // Skip unknown fields.
+                    codedStream.skipField(nextTag);
+                    break;
+            }
+            if (EARLY_RETURN_ENABLED && isMetadataComplete(builder, hasSeenKey)) {
+                break;
+            }
+            nextTag = codedStream.readTag();
+        }
+        return builder.build();
+    }
+
+    private static boolean isMetadataComplete(
+            TemporaryExposureKeyExport.Builder builder, boolean hasSeenKey) {
+        return hasSeenKey
+                && builder.hasStartTimestamp()
+                && builder.hasEndTimestamp()
+                && builder.hasRegion()
+                && builder.hasBatchNum()
+                && builder.hasBatchSize()
+                && builder.getSignatureInfosCount() > 0;
+    }
+
+    private TemporaryExposureKeyFileMetadataParser() {
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TimeAndAttenuation.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TimeAndAttenuation.java
new file mode 100644
index 0000000..996ba5f
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TimeAndAttenuation.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import com.google.auto.value.AutoValue;
+
+/** A pair of time and attenuation. */
+@AutoValue
+abstract class TimeAndAttenuation {
+    static TimeAndAttenuation create(Integer time, Integer attenuation) {
+        return new AutoValue_TimeAndAttenuation(time, attenuation);
+    }
+
+    abstract Integer time();
+
+    abstract Integer attenuation();
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TracingParams.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TracingParams.java
new file mode 100644
index 0000000..cd2ad2b
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/TracingParams.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.matching;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+import com.google.samples.exposurenotification.storage.TracingParamsRecord;
+
+import org.joda.time.Duration;
+
+/** Parameters required for {@link ExposureMatchingTracer}. */
+@AutoValue
+public abstract class TracingParams {
+
+    /**
+     * Minimum consecutive sightings in duration required to be considered a contact, compared with a
+     * bucketized duration calculation. See {@link ExposureMatchingTracer} for more details. If not
+     * set, has default value {@link ContactTracingFeature#minExposureBucketizedDurationSeconds()}
+     * converted to {@link Duration}.
+     */
+    public abstract Duration minExposureBucketizedDuration();
+
+    /**
+     * Minimum ble signal strength indicator value that must be reached by at least one sighting in a
+     * series of sightings that are being considered for an exposure. If not set, has default value
+     * {@link ContactTracingFeature#defaultMinExposureAttenuationValue()} converted to an int.
+     */
+    // TODO(wukevin): remove this field.
+    public abstract int minimumAttenuationValue();
+
+    /**
+     * Maximum duration gap between two sightings to be considered continuous contact with another
+     * device. If not set, has default value {@link
+     * ContactTracingFeature#maxInterpolationDurationSeconds()} converted to {@link Duration}.
+     */
+    public abstract Duration maxInterpolationDuration();
+
+    /**
+     * Bluetooth token scanning interval. If not set, has default value {@link
+     * ContactTracingFeature#scanIntervalSeconds()} converted to {@link Duration}.
+     */
+    public abstract Duration scanInterval();
+
+    /**
+     * Boolean controlling the use of interpolated attentuation levels when computing duration at
+     * thresholds. If not set, defaults to false.
+     *
+     * <p>TODO(b/159218245): add ContactTracingFeature flag for a default value.
+     */
+    public abstract Boolean interpolationEnabled();
+
+    public static Builder builder() {
+        return new AutoValue_TracingParams.Builder()
+                .setMinExposureBucketizedDuration(
+                        Duration.standardSeconds(ContactTracingFeature.minExposureBucketizedDurationSeconds()))
+                .setMinimumAttenuationValue(
+                        (int) ContactTracingFeature.defaultMinExposureAttenuationValue())
+                .setMaxInterpolationDuration(
+                        Duration.standardSeconds(ContactTracingFeature.maxInterpolationDurationSeconds()))
+                .setScanInterval(Duration.standardSeconds(ContactTracingFeature.scanIntervalSeconds()))
+                .setInterpolationEnabled(false);
+    }
+
+    public TracingParamsRecord toTracingParamsRecord() {
+        return TracingParamsRecord.newBuilder()
+                .setMinAttenuationValue(minimumAttenuationValue())
+                .setMaxInterpolationDurationSeconds(maxInterpolationDuration().getStandardSeconds())
+                .setMinExposureBucketizedDurationSeconds(
+                        minExposureBucketizedDuration().getStandardSeconds())
+                .setScanIntervalSeconds((int) scanInterval().getStandardSeconds())
+                .setInterpolationEnabled(interpolationEnabled())
+                .build();
+    }
+
+    /** Builder clas for {@link TracingParams}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+        public abstract Builder setMinExposureBucketizedDuration(
+                Duration minExposureBucketizedDuration);
+
+        public abstract Builder setMinimumAttenuationValue(int minimumAttenuationValue);
+
+        public abstract Builder setMaxInterpolationDuration(Duration maxInterpolationDuration);
+
+        public abstract Builder setScanInterval(Duration scanInterval);
+
+        public abstract Builder setInterpolationEnabled(boolean interpolationEnabled);
+
+        public abstract TracingParams autoBuild();
+
+        public TracingParams build() {
+            TracingParams tracingParams = autoBuild();
+            // Use reverse logic to get >= equivalent from org.joda.time.Duration api.
+            checkArgument(!tracingParams.minExposureBucketizedDuration().isShorterThan(Duration.ZERO));
+            checkArgument(tracingParams.minimumAttenuationValue() >= 0);
+            checkArgument(tracingParams.scanInterval().isLongerThan(Duration.ZERO));
+            checkArgument(tracingParams.maxInterpolationDuration().isLongerThan(Duration.ZERO));
+            return tracingParams;
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/AdvertisementPacket.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/AdvertisementPacket.java
new file mode 100644
index 0000000..584b720
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/AdvertisementPacket.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Preconditions;
+import com.google.common.primitives.Bytes;
+import com.google.samples.exposurenotification.ble.utils.Constants;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+/**
+ * Fully constructed service data that should be advertised over BLE.
+ */
+public class AdvertisementPacket {
+    private final byte[] id;
+    private final byte[] metadata;
+    private final byte[] encryptedMetadata;
+
+    private AdvertisementPacket(byte[] id, byte[] metadata, byte[] encryptedMetadata) {
+        this.id = id;
+        this.metadata = metadata;
+        this.encryptedMetadata = encryptedMetadata;
+    }
+
+    /**
+     * Retrieves fully constructed service data.
+     */
+    public byte[] getPacket() {
+        return Bytes.concat(id, encryptedMetadata);
+    }
+
+    /**
+     * Returns the rolling proximity ID.
+     *
+     * <p>This method returns the mutable reference to the byte array. Use with caution!
+     */
+    public byte[] getIdDirect() {
+        return id;
+    }
+
+    /**
+     * Returns the raw (unencrypted) metadata for debugging purpose.
+     *
+     * <p>This method returns the mutable reference to the byte array. Use with caution!
+     */
+    public byte[] getRawMetadataDirect() {
+        return metadata;
+    }
+
+    /**
+     * Returns the encrypted metadata.
+     *
+     * <p>This method returns the mutable reference to the byte array. Use with caution!
+     */
+    public byte[] getEncryptedMetadataDirect() {
+        return encryptedMetadata;
+    }
+
+    @Override
+    public String toString() {
+        return "ID: "
+                + Constants.fromIdByteArrayToString(id)
+                + " Metadata: "
+                + Constants.fromIdByteArrayToString(encryptedMetadata);
+    }
+
+    /**
+     * Builder that assists with constructing valid AdvertisementPackets.
+     */
+    public static class Builder {
+        @Nullable
+        private byte[] id;
+        @Nullable
+        private byte[] metadata;
+        @Nullable
+        private byte[] encryptedMetadata;
+
+        /**
+         * Sets the rotating proximity ID that should be put into the advertisement packet.
+         *
+         * @throws IllegalArgumentException If the {@code id} is not the right length.
+         */
+        public Builder setProxId(byte[] id) {
+            Preconditions.checkArgument(
+                    id.length == ContactTracingFeature.contactIdLength(),
+                    "Invalid prox ID length %s",
+                    id.length);
+
+            this.id = id;
+
+            return this;
+        }
+
+        /**
+         * Sets the raw metadata.
+         *
+         * <p>Raw metadata is for debugging purpose only. It's not put into the advertisement packet.
+         *
+         * @throws IllegalArgumentException if the {@code metadata} is not the right length.
+         */
+        public Builder setRawMetadata(byte[] metadata) {
+            Preconditions.checkArgument(
+                    metadata.length == Constants.METADATA_LENGTH,
+                    "Invalid metadata length %s",
+                    metadata.length);
+
+            this.metadata = metadata;
+            return this;
+        }
+
+        /**
+         * Sets the encrypted metadata that should be put into the advertisement packet.
+         *
+         * @throws IllegalArgumentException if the {@code encryptedMetadata} is not the right length.
+         */
+        public Builder setEncryptedMetadata(byte[] encryptedMetadata) {
+            Preconditions.checkArgument(
+                    encryptedMetadata.length == Constants.METADATA_LENGTH,
+                    "Invalid encryptedMetadata length %s",
+                    encryptedMetadata.length);
+
+            this.encryptedMetadata = encryptedMetadata;
+            return this;
+        }
+
+        /**
+         * Constructs an advertisement packet.
+         */
+        public AdvertisementPacket build() {
+            Preconditions.checkNotNull(id, "id must be set.");
+            Preconditions.checkNotNull(metadata, "metadata must be set.");
+            Preconditions.checkNotNull(encryptedMetadata, "encryptedMetadata must be set.");
+            return new AdvertisementPacket(id, metadata, encryptedMetadata);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/CalibrationConfidence.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/CalibrationConfidence.java
new file mode 100644
index 0000000..6462cff
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/CalibrationConfidence.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.IntDef;
+
+import com.google.samples.exposurenotification.ExposureNotificationEnums;
+
+/**
+ * Calibration confidence defined for an {@link ExposureWindow}.
+ */
+@IntDef({
+        CalibrationConfidence.LOWEST,
+        CalibrationConfidence.LOW,
+        CalibrationConfidence.MEDIUM,
+        CalibrationConfidence.HIGH,
+})
+public @interface CalibrationConfidence {
+    /**
+     * No calibration data, using fleet-wide as default options.
+     */
+    int LOWEST = ExposureNotificationEnums.CalibrationConfidence.LOWEST_CONFIDENCE_VALUE;
+
+    /**
+     * Using average calibration over models from manufacturer.
+     */
+    int LOW = ExposureNotificationEnums.CalibrationConfidence.LOW_CONFIDENCE_VALUE;
+
+    /**
+     * Using single-antenna orientation for a similar model.
+     */
+    int MEDIUM = ExposureNotificationEnums.CalibrationConfidence.MEDIUM_CONFIDENCE_VALUE;
+
+    /**
+     * Using significant calibration data for this model.
+     */
+    int HIGH = ExposureNotificationEnums.CalibrationConfidence.HIGH_CONFIDENCE_VALUE;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DailySummariesConfig.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DailySummariesConfig.java
new file mode 100644
index 0000000..94c15a1
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DailySummariesConfig.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.FloatRange;
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.samples.exposurenotification.ExposureNotificationEnums;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Configuration of per-day summary of exposures.
+ *
+ * <p>During summarization the following are computed for each {@link ExposureWindow}s:
+ *
+ * <ul>
+ *   <li>a weighted duration, computed as
+ *       <p><code>
+ *          ( immediateDurationSeconds * immediateDurationWeight ) +
+ *          ( nearDurationSeconds      * nearDurationWeight ) +
+ *          ( mediumDurationSeconds    * mediumDurationWeight ) +
+ *          ( otherDurationSeconds     * otherDurationWeight )
+ *       </code>
+ *   <li>a score, computed as
+ *       <p><code>
+ *          reportTypeWeights[Tek.reportType] *
+ *          infectiousnessWeights[infectiousness] *
+ *          weightedDuration
+ *       </code>
+ *       <p>where infectiousness and reportType are set based on the ExposureWindow's diagnosis key
+ *       and the DiagnosisKeysDataMapping
+ * </ul>
+ *
+ * <p>The {@link ExposureWindow}s are then filtered, removing those with score lower than {@link
+ * #getMinimumWindowScore()}.
+ *
+ * <p>Scores and weighted durations of the {@link ExposureWindow}s that pass the {@link
+ * #getMinimumWindowScore()} are then aggregated over a day to compute the maximum and cumulative
+ * scores and duration:
+ *
+ * <ul>
+ *   <li>sumScore = sum(score of ExposureWindows)
+ *   <li>maxScore = max(score of ExposureWindows)
+ *   <li>weightedDurationSum = sum(weighted duration of ExposureWindow)
+ * </ul>
+ *
+ * <p>Note that when the weights are typically around 100% (1.0), both the scores and the
+ * weightedDurationSum can be considered as being expressed in seconds. For example, 15 minutes of
+ * exposure with all weights equal to 1.0 would be 60 * 15 = 900 (seconds).
+ */
+public class DailySummariesConfig {
+
+    List<Double> reportTypeWeights;
+    List<Double> infectiousnessWeights;
+    List<Integer> attenuationBucketThresholdDb;
+    List<Double> attenuationBucketWeights;
+    int daysSinceExposureThreshold;
+    double minimumWindowScore;
+
+    DailySummariesConfig(
+            List<Double> reportTypeWeights,
+            List<Double> infectiousnessWeights,
+            List<Integer> attenuationBucketThresholdDb,
+            List<Double> attenuationBucketWeights,
+            int daysSinceExposureThreshold,
+            double minimumWindowScore) {
+        this.reportTypeWeights = reportTypeWeights;
+        this.infectiousnessWeights = infectiousnessWeights;
+        this.attenuationBucketThresholdDb = attenuationBucketThresholdDb;
+        this.attenuationBucketWeights = attenuationBucketWeights;
+        this.daysSinceExposureThreshold = daysSinceExposureThreshold;
+        this.minimumWindowScore = minimumWindowScore;
+    }
+
+    /**
+     * Scoring weights to associate with exposures with different {@link ReportType}s.
+     *
+     * <p>This map can include weights for the following {@link ReportType}s:
+     *
+     * <ul>
+     *   <li>CONFIRMED_TEST
+     *   <li>CONFIRMED_CLINICAL_DIAGNOSIS
+     *   <li>SELF_REPORT
+     *   <li>RECURSIVE (reserved for future use)
+     * </ul>
+     * <p>
+     * Each element must be between 0 and 2.5.
+     */
+    public Map<Integer, Double> getReportTypeWeights() {
+        HashMap<Integer, Double> result = new HashMap<>();
+        for (int i = 0; i < reportTypeWeights.size(); i++) {
+            result.put(i, reportTypeWeights.get(i));
+        }
+        return result;
+    }
+
+    /**
+     * Scoring weights to associate with exposures with different {@link Infectiousness}.
+     *
+     * <p>This map can include weights for the following {@link Infectiousness} values:
+     *
+     * <ul>
+     *   <li>STANDARD
+     *   <li>HIGH
+     * </ul>
+     * <p>
+     * Each element must be between 0 and 2.5.
+     */
+    public Map<Integer, Double> getInfectiousnessWeights() {
+        HashMap<Integer, Double> result = new HashMap<>();
+        for (int i = 0; i < infectiousnessWeights.size(); i++) {
+            result.put(i, infectiousnessWeights.get(i));
+        }
+        return result;
+    }
+
+    /**
+     * Thresholds defining the BLE attenuation buckets edges.
+     *
+     * <p>This list must have 3 elements: the immediate, near, and medium thresholds. See
+     * attenuationBucketWeights for more information.
+     *
+     * <p>These elements must be between 0 and 255 and come in increasing order.
+     */
+    public List<Integer> getAttenuationBucketThresholdDb() {
+        return new ArrayList<>(attenuationBucketThresholdDb);
+    }
+
+    /**
+     * Scoring weights to associate with ScanInstances depending on the attenuation bucket in which
+     * their typicalAttenuationDb falls.
+     *
+     * <p>This list must have 4 elements, corresponding to the weights for the 4 buckets.
+     *
+     * <ul>
+     *   <li>immediate bucket: -infinity < attenuation <= immediate threshold
+     *   <li>near bucket: immediate threshold < attenuation <= near threshold
+     *   <li>medium bucket: near threshold < attenuation <= medium threshold
+     *   <li>other bucket: medium threshold < attenuation < +infinity
+     * </ul>
+     * <p>
+     * Each element must be between 0 and 2.5.
+     */
+    public List<Double> getAttenuationBucketWeights() {
+        return new ArrayList<>(attenuationBucketWeights);
+    }
+
+    /**
+     * Day summaries older than this are not returned.
+     *
+     * <p>To return all available day summaries, set to 0, which is treated specially.
+     */
+    public int getDaysSinceExposureThreshold() {
+        return daysSinceExposureThreshold;
+    }
+
+    /**
+     * Minimum score that ExposureWindows must reach in order to be included in the {@link
+     * DailySummary.ExposureSummaryData}.
+     *
+     * <p>Use 0 to consider all {@link ExposureWindow}s (recommended).
+     */
+    public double getMinimumWindowScore() {
+        return minimumWindowScore;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof DailySummariesConfig) {
+            DailySummariesConfig that = (DailySummariesConfig) obj;
+            return reportTypeWeights.equals(that.reportTypeWeights)
+                    && infectiousnessWeights.equals(that.infectiousnessWeights)
+                    && attenuationBucketThresholdDb.equals(that.attenuationBucketThresholdDb)
+                    && attenuationBucketWeights.equals(that.attenuationBucketWeights)
+                    && daysSinceExposureThreshold == that.daysSinceExposureThreshold
+                    && minimumWindowScore == that.minimumWindowScore;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(
+                reportTypeWeights,
+                infectiousnessWeights,
+                attenuationBucketThresholdDb,
+                attenuationBucketWeights,
+                daysSinceExposureThreshold,
+                minimumWindowScore);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.US,
+                "DailySummariesConfig<"
+                        + "reportTypeWeights: %s, "
+                        + "infectiousnessWeights: %s, "
+                        + "attenuationBucketThresholdDb: %s, "
+                        + "attenuationBucketWeights: %s"
+                        + "daysSinceExposureThreshold: %d,"
+                        + "minimumWindowScore: %.3f>",
+                reportTypeWeights,
+                infectiousnessWeights,
+                attenuationBucketThresholdDb,
+                attenuationBucketWeights,
+                daysSinceExposureThreshold,
+                minimumWindowScore);
+    }
+
+    /**
+     * Definition of attenuation bucket group.
+     *
+     * @hide
+     */
+    public enum DistanceEstimate {
+        IMMEDIATE,
+        NEAR,
+        MEDIUM,
+        OTHER;
+
+        public static DistanceEstimate getDistanceEstimate(
+                int typicalAttenuation, DailySummariesConfig config) {
+            if (typicalAttenuation <= config.getAttenuationBucketThresholdDb().get(0)) {
+                return IMMEDIATE;
+            } else if (typicalAttenuation <= config.getAttenuationBucketThresholdDb().get(1)) {
+                return NEAR;
+            } else if (typicalAttenuation <= config.getAttenuationBucketThresholdDb().get(2)) {
+                return MEDIUM;
+            } else {
+                return OTHER;
+            }
+        }
+    }
+
+    /**
+     * A builder for {@link DailySummariesConfig}.
+     */
+    public static final class DailySummariesConfigBuilder {
+        private Double[] reportTypeWeights = new Double[6]; // 6 = number of ReportType values.
+        private Double[] infectiousnessWeights =
+                new Double[ExposureNotificationEnums.Infectiousness.values().length];
+        private List<Integer> attenuationBucketThresholdDb;
+        private List<Double> attenuationBucketWeights;
+        int daysSinceExposureThreshold = 0;
+        double minimumWindowScore = 0;
+
+        public DailySummariesConfigBuilder() {
+            Arrays.fill(reportTypeWeights, 0.0);
+            Arrays.fill(infectiousnessWeights, 0.0);
+        }
+
+        /**
+         * See {@link DailySummariesConfig#getReportTypeWeights()}.
+         */
+        public DailySummariesConfigBuilder setReportTypeWeight(
+                @ReportType int reportType, @FloatRange(from = 0.0, to = 2.5) double weight) {
+            checkArgument(
+                    reportType >= ReportType.UNKNOWN && reportType <= ReportType.REVOKED,
+                    "Incorrect value of reportType");
+            checkSingleWeightRange(weight, "reportTypeWeights");
+            this.reportTypeWeights[reportType] = weight;
+            return this;
+        }
+
+        /**
+         * See {@link DailySummariesConfig#getInfectiousnessWeights()}
+         */
+        public DailySummariesConfigBuilder setInfectiousnessWeight(
+                @Infectiousness int infectiousness, @FloatRange(from = 0.0, to = 2.5) double weight) {
+            checkArgument(
+                    ExposureNotificationEnums.Infectiousness.forNumber(infectiousness) != null,
+                    "Incorrect value of infectiousness");
+            checkSingleWeightRange(weight, "infectiousnessWeights");
+            this.infectiousnessWeights[infectiousness] = weight;
+            return this;
+        }
+
+        /**
+         * See {@link DailySummariesConfig#getAttenuationBucketThresholdDb()} and {@link
+         * DailySummariesConfig#getAttenuationBucketWeights()} ()}
+         */
+        public DailySummariesConfigBuilder setAttenuationBuckets(
+                List<Integer> thresholds, List<Double> weights) {
+            setAttenuationBucketThreshold(thresholds);
+            setAttenuationBucketWeights(weights);
+            return this;
+        }
+
+        private void setAttenuationBucketThreshold(List<Integer> attenuationBucketThresholdDb) {
+            checkListSize(
+                    attenuationBucketThresholdDb,
+                    DistanceEstimate.values().length - 1,
+                    "attenuationBucketThresholdDb");
+            for (int i = 0; i < attenuationBucketThresholdDb.size(); i++) {
+                checkArgument(
+                        attenuationBucketThresholdDb.get(i) >= 0 && attenuationBucketThresholdDb.get(i) <= 255,
+                        "Element of attenuationBucketThreshold must between 0 ~ 255");
+                if (i == 0) {
+                    continue;
+                }
+                checkArgument(
+                        attenuationBucketThresholdDb.get(i - 1) < attenuationBucketThresholdDb.get(i),
+                        String.format(
+                                Locale.ENGLISH,
+                                "attenuationBucketThresholdDb of index %d must be larger than index %d",
+                                i,
+                                i - 1));
+            }
+            this.attenuationBucketThresholdDb = new ArrayList<>(attenuationBucketThresholdDb);
+        }
+
+        private void setAttenuationBucketWeights(List<Double> attenuationBucketWeights) {
+            checkListSize(
+                    attenuationBucketWeights, DistanceEstimate.values().length, "attenuationBucketWeights");
+            checkWeightRange(attenuationBucketWeights, "attenuationBucketWeights");
+            this.attenuationBucketWeights = new ArrayList<>(attenuationBucketWeights);
+        }
+
+        /**
+         * See {@link DailySummariesConfig#getDaysSinceExposureThreshold()}
+         */
+        public DailySummariesConfigBuilder setDaysSinceExposureThreshold(
+                int daysSinceExposureThreshold) {
+            checkArgument(
+                    daysSinceExposureThreshold >= 0, "daysSinceExposureThreshold must not be negative");
+            this.daysSinceExposureThreshold = daysSinceExposureThreshold;
+            return this;
+        }
+
+        /**
+         * See {@link DailySummariesConfig#getMinimumWindowScore()}
+         */
+        public DailySummariesConfigBuilder setMinimumWindowScore(double minimumWindowScore) {
+            checkArgument(minimumWindowScore >= 0, "minimumWindowScore must not be negative");
+            this.minimumWindowScore = minimumWindowScore;
+            return this;
+        }
+
+        public DailySummariesConfig build() {
+            checkArgument(attenuationBucketThresholdDb != null, "Must set attenuationBucketThresholdDb");
+            checkArgument(attenuationBucketWeights != null, "Must set attenuationBucketWeights");
+            return new DailySummariesConfig(
+                    Arrays.asList(reportTypeWeights),
+                    Arrays.asList(infectiousnessWeights),
+                    attenuationBucketThresholdDb,
+                    attenuationBucketWeights,
+                    daysSinceExposureThreshold,
+                    minimumWindowScore);
+        }
+
+        @SuppressWarnings("rawtypes")
+        private static void checkListSize(List list, int expectSize, String name) {
+            checkArgument(list != null, String.format(Locale.ENGLISH, "%s must not be null", name));
+            checkArgument(
+                    list.size() == expectSize,
+                    String.format(Locale.ENGLISH, "%s must must contains %d elements", name, expectSize));
+        }
+
+        private static void checkWeightRange(List<Double> values, String name) {
+            for (double value : values) {
+                checkSingleWeightRange(value, name);
+            }
+        }
+
+        private static void checkSingleWeightRange(double value, String name) {
+            checkArgument(
+                    value >= 0 && value <= 2.5,
+                    String.format(Locale.ENGLISH, "Element value of %s must between 0 ~ 2.5", name));
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DailySummary.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DailySummary.java
new file mode 100644
index 0000000..3df3999
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DailySummary.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import android.os.Parcel;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.samples.exposurenotification.annotations.Hide;
+import com.google.samples.exposurenotification.nearby.DailySummary.ExposureSummaryData.ExposureSummaryDataBuilder;
+import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
+import com.google.samples.exposurenotification.safeparcel.ReflectedParcelable;
+import com.google.samples.exposurenotification.safeparcel.SafeParcelable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+
+/** Daily exposure summary to pass to client side. */
+@SafeParcelable.Class(creator = "DailySummaryCreator")
+public class DailySummary extends AbstractSafeParcelable implements ReflectedParcelable {
+
+    @Field(id = 1, getter = "getDaysSinceEpoch")
+    int daysSinceEpoch;
+
+    @Field(id = 2, getter = "getReportSummaries")
+    List<ExposureSummaryData> reportSummaries;
+
+    @Field(id = 3, getter = "getSummaryData")
+    ExposureSummaryData summaryData;
+
+    @Constructor
+    DailySummary(
+            @Param(id = 1) int daysSinceEpoch,
+            @Param(id = 2) List<ExposureSummaryData> reportSummaries,
+            @Param(id = 3) ExposureSummaryData summaryData) {
+        this.daysSinceEpoch = daysSinceEpoch;
+        this.reportSummaries = reportSummaries;
+        this.summaryData = summaryData;
+    }
+
+    /** Returns days since epoch of the {@link ExposureWindow}s that went into this summary. */
+    public int getDaysSinceEpoch() {
+        return daysSinceEpoch;
+    }
+
+    /** Summary of all exposures on this day of a specific diagnosis {@link ReportType}. */
+    public ExposureSummaryData getSummaryDataForReportType(@ReportType int reportType) {
+        return reportSummaries.get(reportType);
+    }
+
+    /** For {@link #CREATOR} to serialize {@link #reportSummaries}. */
+    List<ExposureSummaryData> getReportSummaries() {
+        return reportSummaries;
+    }
+
+    /** Summary of all exposures on this day. */
+    public ExposureSummaryData getSummaryData() {
+        return summaryData;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof DailySummary) {
+            DailySummary that = (DailySummary) obj;
+            return daysSinceEpoch == that.daysSinceEpoch
+                    && reportSummaries.equals(that.reportSummaries)
+                    && Objects.equal(summaryData, that.summaryData);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(daysSinceEpoch, reportSummaries, summaryData);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.US,
+                "DailySummary<" + "daysSinceEpoch: %d, " + "reportSummaries: %s, " + "daySummary: %s>",
+                daysSinceEpoch,
+                reportSummaries,
+                summaryData);
+    }
+
+    /**
+     * A builder for {@link DailySummary}.
+     *
+     * @hide
+     */
+    @Hide
+    public static final class DailySummaryBuilder {
+        private int daysSinceEpoch = 0;
+        private ExposureSummaryData[] reportSummaries =
+                new ExposureSummaryData[6]; // 6 = number of possible ReportTypes.
+        private ExposureSummaryData summaryData;
+
+        public DailySummaryBuilder() {
+            Arrays.fill(reportSummaries, new ExposureSummaryDataBuilder().build());
+        }
+
+        public DailySummaryBuilder setDaysSinceEpoch(int daysSinceEpoch) {
+            this.daysSinceEpoch = daysSinceEpoch;
+            return this;
+        }
+
+        public DailySummaryBuilder setReportSummary(
+                @ReportType int reportType, ExposureSummaryData reportTypeSummaryData) {
+            this.reportSummaries[reportType] = reportTypeSummaryData;
+            return this;
+        }
+
+        public DailySummaryBuilder setSummaryData(ExposureSummaryData summaryData) {
+            this.summaryData = summaryData;
+            return this;
+        }
+
+        public DailySummary build() {
+            return new DailySummary(daysSinceEpoch, Arrays.asList(reportSummaries), summaryData);
+        }
+    }
+
+    /** Stores different scores for specific {@link ReportType}. */
+    @SafeParcelable.Class(creator = "ExposureSummaryDataCreator")
+    public static class ExposureSummaryData extends AbstractSafeParcelable
+            implements ReflectedParcelable {
+
+        @Field(id = 1, getter = "getMaximumScore")
+        double maximumScore;
+
+        @Field(id = 2, getter = "getScoreSum")
+        double scoreSum;
+
+        @Field(id = 3, getter = "getWeightedDurationSum")
+        double weightedDurationSum;
+
+        @Constructor
+        ExposureSummaryData(
+                @Param(id = 1) double maximumScore,
+                @Param(id = 2) double scoreSum,
+                @Param(id = 3) double weightedDurationSum) {
+            this.maximumScore = maximumScore;
+            this.scoreSum = scoreSum;
+            this.weightedDurationSum = weightedDurationSum;
+        }
+
+        /**
+         * Highest score of all {@link ExposureWindow}s aggregated into this summary.
+         *
+         * <p>See {@link DailySummariesConfig} for more information about how the per-ExposureWindow
+         * score is computed.
+         */
+        public double getMaximumScore() {
+            return maximumScore;
+        }
+
+        /**
+         * Sum of scores for all {@link ExposureWindow}s aggregated into this summary.
+         *
+         * <p>See {@link DailySummariesConfig} for more information about how the per-ExposureWindow
+         * score is computed.
+         */
+        public double getScoreSum() {
+            return scoreSum;
+        }
+
+        /**
+         * Sum of weighted durations for all {@link ExposureWindow}s aggregated into this summary.
+         *
+         * <p>See {@link DailySummariesConfig} for more information about how the per-{@link
+         * ExposureWindow} score and weightedDurationSum are computed.
+         */
+        public double getWeightedDurationSum() {
+            return weightedDurationSum;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            if (obj instanceof ExposureSummaryData) {
+                ExposureSummaryData that = (ExposureSummaryData) obj;
+                return maximumScore == that.maximumScore
+                        && scoreSum == that.scoreSum
+                        && weightedDurationSum == that.weightedDurationSum;
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(maximumScore, scoreSum, weightedDurationSum);
+        }
+
+        @Override
+        public String toString() {
+            return String.format(
+                    Locale.US,
+                    "ExposureSummaryData<"
+                            + "maximumScore: %.3f, "
+                            + "scoreSum: %.3f, "
+                            + "weightedDurationSum: %.3f>",
+                    maximumScore,
+                    scoreSum,
+                    weightedDurationSum);
+        }
+
+        /** A builder for {@link ExposureSummaryData}. @hide */
+        @Hide
+        public static final class ExposureSummaryDataBuilder {
+            private double maximumScore = 0;
+            private double scoreSum = 0;
+            private double weightedDurationSum = 0;
+
+            public ExposureSummaryDataBuilder setMaximumScore(double maximumScore) {
+                this.maximumScore = maximumScore;
+                return this;
+            }
+
+            public ExposureSummaryDataBuilder setScoreSum(double scoreSum) {
+                this.scoreSum = scoreSum;
+                return this;
+            }
+
+            public ExposureSummaryDataBuilder setWeightedDurationSum(double weightedDurationSum) {
+                this.weightedDurationSum = weightedDurationSum;
+                return this;
+            }
+
+            public ExposureSummaryData build() {
+                return new ExposureSummaryData(maximumScore, scoreSum, weightedDurationSum);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DiagnosisKeyFileProvider.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DiagnosisKeyFileProvider.java
new file mode 100644
index 0000000..57992d2
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DiagnosisKeyFileProvider.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.util.List;
+
+/**
+ * Provider which holds a list of diagnosis key files and can open/supply them one by one as they
+ * are ready to be processed.
+ */
+public class DiagnosisKeyFileProvider {
+
+    private int index = 0;
+    private final List<File> files;
+
+    public DiagnosisKeyFileProvider(List<File> files) {
+        this.files = ImmutableList.copyOf(files);
+    }
+
+    /**
+     * Checks whether another File is available.
+     *
+     * @hide
+     */
+    public boolean hasNext() {
+        return files.size() > index;
+    }
+
+    /**
+     * Gets the current file and increments the pointer to the next.
+     *
+     * @hide
+     */
+    public File getNext() {
+        return files.get(index++);
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DiagnosisKeysDataMapping.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DiagnosisKeysDataMapping.java
new file mode 100644
index 0000000..a62d790
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/DiagnosisKeysDataMapping.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.samples.exposurenotification.ExposureNotificationEnums;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/** Mappings from diagnosis keys data to concepts returned by the API. */
+public class DiagnosisKeysDataMapping {
+    /**
+     * The maximum days since onset for checking the valid range of {@link
+     * #getDaysSinceOnsetToInfectiousness}.
+     *
+     * @hide
+     */
+    public static final int MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET = 14;
+
+    /**
+     * The maximum size of {@link #getDaysSinceOnsetToInfectiousness}.
+     *
+     * @hide
+     */
+    public static final int SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS =
+            MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET * 2 + 1;
+
+    List<Integer> daysSinceOnsetToInfectiousness;
+
+    @ReportType
+    int reportTypeWhenMissing;
+
+    @Infectiousness
+    int infectiousnessWhenDaysSinceOnsetMissing;
+
+    DiagnosisKeysDataMapping(
+            List<Integer> daysSinceOnsetToInfectiousness,
+            @ReportType int reportTypeWhenMissing,
+            @Infectiousness int infectiousnessWhenDaysSinceOnsetMissing) {
+        this.daysSinceOnsetToInfectiousness = daysSinceOnsetToInfectiousness;
+        this.reportTypeWhenMissing = reportTypeWhenMissing;
+        this.infectiousnessWhenDaysSinceOnsetMissing = infectiousnessWhenDaysSinceOnsetMissing;
+    }
+
+    /**
+     * Mapping from diagnosisKey.daysSinceOnsetOfSymptoms to {@link Infectiousness}.
+     *
+     * <p>Infectiousness is computed from this mapping and the tek metadata as -
+     * daysSinceOnsetToInfectiousness[{@link TemporaryExposureKey#getDaysSinceOnsetOfSymptoms}], or -
+     * {@link #getInfectiousnessWhenDaysSinceOnsetMissing} if {@link
+     * TemporaryExposureKey#getDaysSinceOnsetOfSymptoms} is {@link
+     * TemporaryExposureKey#DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN}.
+     *
+     * <p>Values of DaysSinceOnsetOfSymptoms that aren't represented in this map are given {@link
+     * Infectiousness#NONE} as infectiousness. Exposures with infectiousness equal to {@link
+     * Infectiousness#NONE} are dropped.
+     */
+    public Map<Integer, Integer> getDaysSinceOnsetToInfectiousness() {
+        HashMap<Integer, Integer> map = Maps.newHashMapWithExpectedSize(29);
+        for (int i = 0; i < daysSinceOnsetToInfectiousness.size(); i++) {
+            map.put(i - 14, daysSinceOnsetToInfectiousness.get(i));
+        }
+        return map;
+    }
+
+    /**
+     * Report type to default to when a TEK has no report type set.
+     *
+     * <p>This report type gets used when creating the {@link ExposureWindow}s and the {@link
+     * DailySummary}s. The system will treat TEKs with missing report types as if they had this
+     * provided report type.
+     */
+    @ReportType
+    public int getReportTypeWhenMissing() {
+        return reportTypeWhenMissing;
+    }
+
+    /**
+     * Infectiousness of TEKs for which onset of symptoms is not set.
+     *
+     * <p>See {@link #getDaysSinceOnsetToInfectiousness} for more info.
+     */
+    @Infectiousness
+    public int getInfectiousnessWhenDaysSinceOnsetMissing() {
+        return infectiousnessWhenDaysSinceOnsetMissing;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof DiagnosisKeysDataMapping) {
+            DiagnosisKeysDataMapping that = (DiagnosisKeysDataMapping) obj;
+            return daysSinceOnsetToInfectiousness.equals(that.daysSinceOnsetToInfectiousness)
+                    && reportTypeWhenMissing == that.reportTypeWhenMissing
+                    && infectiousnessWhenDaysSinceOnsetMissing
+                    == that.infectiousnessWhenDaysSinceOnsetMissing;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(
+                daysSinceOnsetToInfectiousness,
+                reportTypeWhenMissing,
+                infectiousnessWhenDaysSinceOnsetMissing);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.US,
+                "DiagnosisKeysDataMapping<"
+                        + "daysSinceOnsetToInfectiousness: %s, "
+                        + "reportTypeWhenMissing: %d, "
+                        + "infectiousnessWhenDaysSinceOnsetMissing: %d>",
+                getDaysSinceOnsetToInfectiousness(),
+                reportTypeWhenMissing,
+                infectiousnessWhenDaysSinceOnsetMissing);
+    }
+
+    /** A builder for {@link DiagnosisKeysDataMapping}. */
+    public static final class DiagnosisKeysDataMappingBuilder {
+        private List<Integer> daysSinceOnsetToInfectiousness = null;
+
+        @ReportType private int reportTypeWhenMissing = ReportType.UNKNOWN;
+
+        @Infectiousness private Integer infectiousnessWhenDaysSinceOnsetMissing = null;
+
+        /**
+         * For internal converting the data back to API surface data structure.
+         *
+         * @hide
+         */
+        public DiagnosisKeysDataMappingBuilder internalSetDaysSinceOnsetToInfectiousness(
+                List<ExposureNotificationEnums.Infectiousness> daysSinceOnsetToInfectiousness) {
+            checkArgument(
+                    daysSinceOnsetToInfectiousness.size() == SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS,
+                    "The size of daysSinceOnsetToInfectiousness must be %d.",
+                    SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS);
+
+            this.daysSinceOnsetToInfectiousness =
+                    Lists.newArrayListWithExpectedSize(SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS);
+            for (ExposureNotificationEnums.Infectiousness infectiousness :
+                    daysSinceOnsetToInfectiousness) {
+                this.daysSinceOnsetToInfectiousness.add(infectiousness.getNumber());
+            }
+            return this;
+        }
+
+        public DiagnosisKeysDataMappingBuilder setDaysSinceOnsetToInfectiousness(
+                Map<Integer, Integer> daysSinceOnsetToInfectiousness) {
+            checkArgument(
+                    daysSinceOnsetToInfectiousness != null,
+                    "daysSinceOnsetToInfectiousness must not be null.");
+            checkArgument(
+                    daysSinceOnsetToInfectiousness.size() <= SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS,
+                    "the size of daysSinceOnsetToInfectiousness exceeds maximum size %d.",
+                    SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS);
+
+            Integer[] infectiousnessArray = new Integer[SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS];
+            Arrays.fill(infectiousnessArray, Infectiousness.NONE);
+            for (Map.Entry<Integer, Integer> entry : daysSinceOnsetToInfectiousness.entrySet()) {
+                int daysSinceOnset = entry.getKey();
+                int infectiousness = entry.getValue();
+                checkArgument(
+                        Math.abs(daysSinceOnset) <= MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET,
+                        "Invalid day since onset %d",
+                        daysSinceOnset);
+                checkArgument(
+                        ExposureNotificationEnums.Infectiousness.forNumber(infectiousness) != null,
+                        "Invalid value of Infectiousness %d",
+                        infectiousness);
+                infectiousnessArray[MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET + daysSinceOnset] = infectiousness;
+            }
+            this.daysSinceOnsetToInfectiousness = Arrays.asList(infectiousnessArray);
+            return this;
+        }
+
+        public DiagnosisKeysDataMappingBuilder setReportTypeWhenMissing(
+                @ReportType int reportTypeWhenMissing) {
+            checkArgument(
+                    reportTypeWhenMissing != ReportType.UNKNOWN, "Invalid reportTypeWhenMissing value");
+            checkArgument(
+                    reportTypeWhenMissing >= ReportType.UNKNOWN
+                            && reportTypeWhenMissing <= ReportType.REVOKED,
+                    "Invalid value of ReportType %d",
+                    reportTypeWhenMissing);
+            this.reportTypeWhenMissing = reportTypeWhenMissing;
+            return this;
+        }
+
+        public DiagnosisKeysDataMappingBuilder setInfectiousnessWhenDaysSinceOnsetMissing(
+                @Infectiousness int infectiousnessWhenDaysSinceOnsetMissing) {
+            checkArgument(
+                    ExposureNotificationEnums.Infectiousness.forNumber(
+                            infectiousnessWhenDaysSinceOnsetMissing)
+                            != null,
+                    "Invalid value of Infectiousness %d",
+                    infectiousnessWhenDaysSinceOnsetMissing);
+            this.infectiousnessWhenDaysSinceOnsetMissing = infectiousnessWhenDaysSinceOnsetMissing;
+            return this;
+        }
+
+        public DiagnosisKeysDataMapping build() {
+            checkArgument(
+                    daysSinceOnsetToInfectiousness != null, "Must set daysSinceOnsetToInfectiousness");
+            checkArgument(reportTypeWhenMissing != ReportType.UNKNOWN, "Must set reportTypeWhenMissing");
+            checkArgument(
+                    infectiousnessWhenDaysSinceOnsetMissing != null,
+                    "Must set infectiousnessWhenDaysSinceOnsetMissing");
+            return new DiagnosisKeysDataMapping(
+                    daysSinceOnsetToInfectiousness,
+                    reportTypeWhenMissing,
+                    infectiousnessWhenDaysSinceOnsetMissing);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureConfiguration.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureConfiguration.java
new file mode 100644
index 0000000..4416137
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureConfiguration.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
+import com.google.samples.exposurenotification.safeparcel.SafeParcelable;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Exposure configuration parameters that can be provided when initializing the service.
+ *
+ * <p>These parameters are used to calculate risk for each exposure incident using the following
+ * formula:
+ *
+ * <p><code>
+ *   RiskScore = attenuationScore
+ *       * daysSinceLastExposureScore
+ *       * durationScore
+ *       * transmissionRiskScore
+ * </code>
+ *
+ * <p>Scores are in the range [0-8]. Weights are in the range [0-100].
+ *
+ * @deprecated no longer used with Exposure Windows API.
+ */
+@SafeParcelable.Class(creator = "ExposureConfigurationCreator")
+@Deprecated
+public final class ExposureConfiguration extends AbstractSafeParcelable {
+
+    @Field(id = 1, getter = "getMinimumRiskScore")
+    int minimumRiskScore;
+
+    @Field(id = 2, getter = "getAttenuationScores")
+    int[] attenuationScores;
+
+    @Field(id = 3, getter = "getAttenuationWeight")
+    int attenuationWeight;
+
+    @Field(id = 4, getter = "getDaysSinceLastExposureScores")
+    int[] daysSinceLastExposureScores;
+
+    @Field(id = 5, getter = "getDaysSinceLastExposureWeight")
+    int daysSinceLastExposureWeight;
+
+    @Field(id = 6, getter = "getDurationScores")
+    int[] durationScores;
+
+    @Field(id = 7, getter = "getDurationWeight")
+    int durationWeight;
+
+    @Field(id = 8, getter = "getTransmissionRiskScores")
+    int[] transmissionRiskScores;
+
+    @Field(id = 9, getter = "getTransmissionRiskWeight")
+    int transmissionRiskWeight;
+
+    @Field(id = 10, getter = "getDurationAtAttenuationThresholds")
+    int[] durationAtAttenuationThresholds;
+
+    @Constructor
+    ExposureConfiguration(
+            @Param(id = 1) int minimumRiskScore,
+            @Param(id = 2) int[] attenuationScores,
+            @Param(id = 3) int attenuationWeight,
+            @Param(id = 4) int[] daysSinceLastExposureScores,
+            @Param(id = 5) int daysSinceLastExposureWeight,
+            @Param(id = 6) int[] durationScores,
+            @Param(id = 7) int durationWeight,
+            @Param(id = 8) int[] transmissionRiskScores,
+            @Param(id = 9) int transmissionRiskWeight,
+            @Param(id = 10) int[] durationAtAttenuationThresholds) {
+        this.minimumRiskScore = minimumRiskScore;
+        this.attenuationScores = attenuationScores;
+        this.attenuationWeight = attenuationWeight;
+        this.daysSinceLastExposureScores = daysSinceLastExposureScores;
+        this.daysSinceLastExposureWeight = daysSinceLastExposureWeight;
+        this.durationScores = durationScores;
+        this.durationWeight = durationWeight;
+        this.transmissionRiskScores = transmissionRiskScores;
+        this.transmissionRiskWeight = transmissionRiskWeight;
+        this.durationAtAttenuationThresholds = durationAtAttenuationThresholds;
+    }
+
+    /**
+     * Minimum risk score. Excludes exposure incidents with scores lower than this from calculation of
+     * {@link ExposureSummary#getMaximumRiskScore}, {@link ExposureSummary#getSummationRiskScore}, and
+     * {@link ExposureInformation#getTotalRiskScore}. Other returned fields are unaffected by this
+     * setting.
+     *
+     * <p>Defaults to no minimum.
+     */
+    public int getMinimumRiskScore() {
+        return minimumRiskScore;
+    }
+
+    /**
+     * Scores for attenuation buckets. Must contain 8 scores, one for each bucket as defined below:
+     *
+     * <p><code>{@code
+     * attenuationScores[0] when Attenuation > 73
+     * attenuationScores[1] when 73 >= Attenuation > 63
+     * attenuationScores[2] when 63 >= Attenuation > 51
+     * attenuationScores[3] when 51 >= Attenuation > 33
+     * attenuationScores[4] when 33 >= Attenuation > 27
+     * attenuationScores[5] when 27 >= Attenuation > 15
+     * attenuationScores[6] when 15 >= Attenuation > 10
+     * attenuationScores[7] when 10 >= Attenuation
+     * }</code>
+     */
+    public int[] getAttenuationScores() {
+        return Arrays.copyOf(attenuationScores, attenuationScores.length);
+    }
+
+    /**
+     * Weight to apply to the attenuation score. Must be in the range 0-100.
+     *
+     * <p>Reserved for future use.
+     */
+    public int getAttenuationWeight() {
+        return attenuationWeight;
+    }
+
+    /**
+     * Scores for days since last exposure buckets. Must contain 8 scores, one for each bucket as
+     * defined below:
+     *
+     * <p><code>{@code
+     * daysSinceLastExposureScores[0] when Days >= 14
+     * daysSinceLastExposureScores[1] when Days >= 12
+     * daysSinceLastExposureScores[2] when Days >= 10
+     * daysSinceLastExposureScores[3] when Days >= 8
+     * daysSinceLastExposureScores[4] when Days >= 6
+     * daysSinceLastExposureScores[5] when Days >= 4
+     * daysSinceLastExposureScores[6] when Days >= 2
+     * daysSinceLastExposureScores[7] when Days >= 0
+     * }</code>
+     */
+    public int[] getDaysSinceLastExposureScores() {
+        return Arrays.copyOf(daysSinceLastExposureScores, daysSinceLastExposureScores.length);
+    }
+
+    /**
+     * Weight to apply to the days since last exposure score. Must be in the range 0-100.
+     *
+     * <p>Reserved for future use.
+     */
+    public int getDaysSinceLastExposureWeight() {
+        return daysSinceLastExposureWeight;
+    }
+
+    /**
+     * Scores for duration buckets. Must contain 8 scores, one for each bucket as defined below:
+     *
+     * <p><code>{@code
+     * durationScores[0] when Duration == 0
+     * durationScores[1] when Duration <= 5
+     * durationScores[2] when Duration <= 10
+     * durationScores[3] when Duration <= 15
+     * durationScores[4] when Duration <= 20
+     * durationScores[5] when Duration <= 25
+     * durationScores[6] when Duration <= 30
+     * durationScores[7] when Duration  > 30
+     * }</code>
+     */
+    public int[] getDurationScores() {
+        return Arrays.copyOf(durationScores, durationScores.length);
+    }
+
+    /**
+     * Weight to apply to the duration score. Must be in the range 0-100.
+     *
+     * <p>Reserved for future use.
+     */
+    public int getDurationWeight() {
+        return durationWeight;
+    }
+
+    /**
+     * Scores for transmission risk buckets. Must contain 8 scores, one for each bucket as defined
+     * below:
+     *
+     * <p><code>{@code
+     * transmissionRiskScores[0] when RISK_SCORE_LOWEST
+     * transmissionRiskScores[1] when RISK_SCORE_LOW
+     * transmissionRiskScores[2] when RISK_SCORE_LOW_MEDIUM
+     * transmissionRiskScores[3] when RISK_SCORE_MEDIUM
+     * transmissionRiskScores[4] when RISK_SCORE_MEDIUM_HIGH
+     * transmissionRiskScores[5] when RISK_SCORE_HIGH
+     * transmissionRiskScores[6] when RISK_SCORE_VERY_HIGH
+     * transmissionRiskScores[7] when RISK_SCORE_HIGHEST
+     * }</code>
+     */
+    public int[] getTransmissionRiskScores() {
+        return Arrays.copyOf(transmissionRiskScores, transmissionRiskScores.length);
+    }
+
+    /**
+     * Weight to apply to the transmission risk score. Must be in the range 0-100.
+     *
+     * <p>Reserved for future use.
+     */
+    public int getTransmissionRiskWeight() {
+        return transmissionRiskWeight;
+    }
+
+    /**
+     * Attenuation thresholds to apply when calculating duration at attenuation. Must contain two
+     * thresholds, each in range of 0 - 255. durationAtAttenuationThresholds[0] has to be <=
+     * durationAtAttenuationThresholds[1]. These are used used to populate {@link
+     * ExposureSummary#getAttenuationDurationsInMinutes} and {@link
+     * ExposureInformation#getAttenuationDurationsInMinutes}.
+     */
+    public int[] getDurationAtAttenuationThresholds() {
+        return Arrays.copyOf(durationAtAttenuationThresholds, durationAtAttenuationThresholds.length);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof ExposureConfiguration) {
+            ExposureConfiguration that = (ExposureConfiguration) obj;
+            return Objects.equal(minimumRiskScore, that.getMinimumRiskScore())
+                    && Arrays.equals(attenuationScores, that.getAttenuationScores())
+                    && Objects.equal(attenuationWeight, that.getAttenuationWeight())
+                    && Arrays.equals(daysSinceLastExposureScores, that.getDaysSinceLastExposureScores())
+                    && Objects.equal(daysSinceLastExposureWeight, that.getDaysSinceLastExposureWeight())
+                    && Arrays.equals(durationScores, that.getDurationScores())
+                    && Objects.equal(durationWeight, that.getDurationWeight())
+                    && Arrays.equals(transmissionRiskScores, that.getTransmissionRiskScores())
+                    && Objects.equal(transmissionRiskWeight, that.getTransmissionRiskWeight())
+                    && Arrays.equals(
+                    durationAtAttenuationThresholds, that.getDurationAtAttenuationThresholds());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(
+                minimumRiskScore,
+                attenuationScores,
+                attenuationWeight,
+                daysSinceLastExposureScores,
+                daysSinceLastExposureWeight,
+                durationScores,
+                durationWeight,
+                transmissionRiskScores,
+                transmissionRiskWeight,
+                durationAtAttenuationThresholds);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.US,
+                "ExposureConfiguration<"
+                        + "minimumRiskScore: %d, "
+                        + "attenuationScores: %s, "
+                        + "attenuationWeight: %d, "
+                        + "daysSinceLastExposureScores: %s, "
+                        + "daysSinceLastExposureWeight: %d, "
+                        + "durationScores: %s, "
+                        + "durationWeight: %d, "
+                        + "transmissionRiskScores: %s, "
+                        + "transmissionRiskWeight: %d, "
+                        + "durationAtAttenuationThresholds: %s"
+                        + ">",
+                minimumRiskScore,
+                Arrays.toString(attenuationScores),
+                attenuationWeight,
+                Arrays.toString(daysSinceLastExposureScores),
+                daysSinceLastExposureWeight,
+                Arrays.toString(durationScores),
+                durationWeight,
+                Arrays.toString(transmissionRiskScores),
+                transmissionRiskWeight,
+                Arrays.toString(durationAtAttenuationThresholds));
+    }
+
+    /** A builder for {@link ExposureConfiguration}. */
+    public static final class ExposureConfigurationBuilder {
+
+        private int minimumRiskScore = 4;
+        private int[] attenuationScores = new int[] {4, 4, 4, 4, 4, 4, 4, 4};
+        private int attenuationWeight = 50;
+        private int[] daysSinceLastExposureScores = new int[] {4, 4, 4, 4, 4, 4, 4, 4};
+        private int daysSinceLastExposureWeight = 50;
+        private int[] durationScores = new int[] {4, 4, 4, 4, 4, 4, 4, 4};
+        private int durationWeight = 50;
+        private int[] transmissionRiskScores = new int[] {4, 4, 4, 4, 4, 4, 4, 4};
+        private int transmissionRiskWeight = 50;
+        private int[] durationAtAttenuationThresholds = new int[] {50, 74};
+
+        public ExposureConfigurationBuilder setMinimumRiskScore(int minimumRiskScore) {
+            checkArgument(
+                    minimumRiskScore >= 1 && minimumRiskScore <= 4096,
+                    "minimumRiskScore (%s) must be >= 1 and <= 4096",
+                    minimumRiskScore);
+            this.minimumRiskScore = minimumRiskScore;
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setAttenuationScores(int... attenuationScores) {
+            checkArgument(
+                    attenuationScores.length == 8,
+                    "attenuationScores (%s) must have exactly 8 elements",
+                    Arrays.toString(attenuationScores));
+            for (int attenuationScore : attenuationScores) {
+                checkArgument(
+                        attenuationScore >= 0 && attenuationScore <= 8,
+                        "attenuationScore (%s) must be >= 0 and <= 8",
+                        attenuationScore);
+            }
+            this.attenuationScores = Arrays.copyOf(attenuationScores, attenuationScores.length);
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setAttenuationWeight(int attenuationWeight) {
+            checkArgument(
+                    attenuationWeight >= 0 && attenuationWeight <= 100,
+                    "attenuationWeight (%s) must be >= 0 and <= 100",
+                    attenuationWeight);
+            this.attenuationWeight = attenuationWeight;
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setDaysSinceLastExposureScores(
+                int... daysSinceLastExposureScores) {
+            checkArgument(
+                    daysSinceLastExposureScores.length == 8,
+                    "daysSinceLastExposureScores (%s) must have exactly 8 elements",
+                    Arrays.toString(daysSinceLastExposureScores));
+            for (int daysSinceLastExposureScore : daysSinceLastExposureScores) {
+                checkArgument(
+                        daysSinceLastExposureScore >= 0 && daysSinceLastExposureScore <= 8,
+                        "daysSinceLastExposureScore (%s) must be >= 0 and <= 8",
+                        daysSinceLastExposureScore);
+            }
+            this.daysSinceLastExposureScores =
+                    Arrays.copyOf(daysSinceLastExposureScores, daysSinceLastExposureScores.length);
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setDaysSinceLastExposureWeight(
+                int daysSinceLastExposureWeight) {
+            checkArgument(
+                    daysSinceLastExposureWeight >= 0 && daysSinceLastExposureWeight <= 100,
+                    "daysSinceLastExposureWeight (%s) must be >= 0 and <= 100",
+                    daysSinceLastExposureWeight);
+            this.daysSinceLastExposureWeight = daysSinceLastExposureWeight;
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setDurationScores(int... durationScores) {
+            checkArgument(
+                    durationScores.length == 8,
+                    "durationScores (%s) must have exactly 8 elements",
+                    Arrays.toString(durationScores));
+            for (int durationScore : durationScores) {
+                checkArgument(
+                        durationScore >= 0 && durationScore <= 8,
+                        "durationScore (%s) must be >= 0 and <= 8",
+                        durationScore);
+            }
+            this.durationScores = Arrays.copyOf(durationScores, durationScores.length);
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setDurationWeight(int durationWeight) {
+            checkArgument(
+                    durationWeight >= 0 && durationWeight <= 100,
+                    "durationWeight (%s) must be >= 0 and <= 100",
+                    durationWeight);
+            this.durationWeight = durationWeight;
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setTransmissionRiskScores(int... transmissionRiskScores) {
+            checkArgument(
+                    transmissionRiskScores.length == 8,
+                    "transmissionRiskScores (%s) must have exactly 8 elements",
+                    Arrays.toString(transmissionRiskScores));
+            for (int transmissionRiskScore : transmissionRiskScores) {
+                checkArgument(
+                        transmissionRiskScore >= 0 && transmissionRiskScore <= 8,
+                        "transmissionRiskScore (%s) must be >= 0 and <= 8",
+                        transmissionRiskScore);
+            }
+            this.transmissionRiskScores =
+                    Arrays.copyOf(transmissionRiskScores, transmissionRiskScores.length);
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setTransmissionRiskWeight(int transmissionRiskWeight) {
+            checkArgument(
+                    transmissionRiskWeight >= 0 && transmissionRiskWeight <= 100,
+                    "transmissionRiskWeight (%s) must be >= 0 and <= 100",
+                    transmissionRiskWeight);
+            this.transmissionRiskWeight = transmissionRiskWeight;
+            return this;
+        }
+
+        public ExposureConfigurationBuilder setDurationAtAttenuationThresholds(
+                int... durationAtAttenuationThresholds) {
+            checkArgument(
+                    durationAtAttenuationThresholds.length == 2,
+                    "durationAtAttenuationThresholds (%s) must have exactly 2 elements",
+                    Arrays.toString(durationAtAttenuationThresholds));
+            for (int durationAtAttenuationThreshold : durationAtAttenuationThresholds) {
+                checkArgument(
+                        durationAtAttenuationThreshold >= 0 && durationAtAttenuationThreshold <= 255,
+                        "durationAtAttenuationThreshold (%s) must be >= 0 and <= 255",
+                        durationAtAttenuationThreshold);
+            }
+            checkArgument(
+                    durationAtAttenuationThresholds[0] <= durationAtAttenuationThresholds[1],
+                    "durationAtAttenuationThresholds[0] (%s) must be <= than"
+                            + " durationAtAttenuationThresholds[1] (%s)",
+                    durationAtAttenuationThresholds[0],
+                    durationAtAttenuationThresholds[1]);
+            this.durationAtAttenuationThresholds = durationAtAttenuationThresholds;
+            return this;
+        }
+
+        public ExposureConfiguration build() {
+            return new ExposureConfiguration(
+                    minimumRiskScore,
+                    attenuationScores,
+                    attenuationWeight,
+                    daysSinceLastExposureScores,
+                    daysSinceLastExposureWeight,
+                    durationScores,
+                    durationWeight,
+                    transmissionRiskScores,
+                    transmissionRiskWeight,
+                    durationAtAttenuationThresholds);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureInformation.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureInformation.java
new file mode 100644
index 0000000..c78d877
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureInformation.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
+import com.google.samples.exposurenotification.safeparcel.SafeParcelable;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+
+/**
+ * Information about an exposure, meaning a single diagnosis key over a contiguous period of time
+ * specified by durationMinutes.
+ *
+ * <p>The client can get the exposure information via {@link
+ * ExposureNotificationClient#getExposureInformation}.
+ *
+ * @deprecated no longer used with Exposure Window API.
+ */
+@SafeParcelable.Class(creator = "ExposureInformationCreator")
+@Deprecated
+public final class ExposureInformation extends AbstractSafeParcelable {
+
+    @Field(id = 1, getter = "getDateMillisSinceEpoch")
+    long dateMillisSinceEpoch;
+
+    @Field(id = 2, getter = "getDurationMinutes")
+    int durationMinutes;
+
+    @Field(id = 3, getter = "getAttenuationValue")
+    int attenuationValue;
+
+    @Field(id = 4, getter = "getTransmissionRiskLevel")
+    @RiskLevel
+    int transmissionRiskLevel;
+
+    @Field(id = 5, getter = "getTotalRiskScore")
+    int totalRiskScore;
+
+    @Field(id = 6, getter = "getAttenuationDurationsInMinutes")
+    int[] attenuationDurations;
+
+    @Constructor
+    ExposureInformation(
+            @Param(id = 1) long dateMillisSinceEpoch,
+            @Param(id = 2) int durationMinutes,
+            @Param(id = 3) int attenuationValue,
+            @Param(id = 4) int transmissionRiskLevel,
+            @Param(id = 5) int totalRiskScore,
+            @Param(id = 6) int[] attenuationDurations) {
+        this.dateMillisSinceEpoch = dateMillisSinceEpoch;
+        this.durationMinutes = durationMinutes;
+        this.attenuationValue = attenuationValue;
+        this.transmissionRiskLevel = transmissionRiskLevel;
+        this.totalRiskScore = totalRiskScore;
+        this.attenuationDurations = attenuationDurations;
+    }
+
+    /** Day-level resolution that the exposure occurred, in milliseconds since epoch. */
+    public long getDateMillisSinceEpoch() {
+        return dateMillisSinceEpoch;
+    }
+
+    /** Length of exposure in 5 minute increments, with a 30 minute maximum. */
+    public int getDurationMinutes() {
+        return durationMinutes;
+    }
+
+    /**
+     * The time-weighted signal strength attenuation value which goes into {@link #getTotalRiskScore}.
+     * Value will be between 0 and 255.
+     */
+    public int getAttenuationValue() {
+        return attenuationValue;
+    }
+
+    /** The transmission risk associated with the matched diagnosis key. */
+    @RiskLevel
+    public int getTransmissionRiskLevel() {
+        return transmissionRiskLevel;
+    }
+
+    /**
+     * The total risk calculated for the exposure. See {@link ExposureConfiguration} for more
+     * information about what is represented by the risk score, it will be a value 0-4096.
+     */
+    public int getTotalRiskScore() {
+        return totalRiskScore;
+    }
+
+    /**
+     * Array of durations in minutes at certain radio signal attenuations. The thresholds are defined
+     * in {@link ExposureConfiguration#getDurationAtAttenuationThresholds}.
+     *
+     * <p>Array index 0: Sum of durations for all exposures when attenuation < low threshold.
+     *
+     * <p>Array index 1: Sum of durations for all exposures when low threshold <= attenuation < high
+     * threshold.
+     *
+     * <p>Array index 2: Sum of durations for all exposures when attenuation >= high threshold.
+     */
+    public int[] getAttenuationDurationsInMinutes() {
+        return Arrays.copyOf(attenuationDurations, attenuationDurations.length);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof ExposureInformation) {
+            ExposureInformation that = (ExposureInformation) obj;
+            return Objects.equal(dateMillisSinceEpoch, that.getDateMillisSinceEpoch())
+                    && Objects.equal(durationMinutes, that.getDurationMinutes())
+                    && Objects.equal(attenuationValue, that.getAttenuationValue())
+                    && Objects.equal(transmissionRiskLevel, that.getTransmissionRiskLevel())
+                    && Objects.equal(totalRiskScore, that.getTotalRiskScore())
+                    && Arrays.equals(attenuationDurations, that.getAttenuationDurationsInMinutes());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(
+                dateMillisSinceEpoch,
+                durationMinutes,
+                attenuationValue,
+                transmissionRiskLevel,
+                totalRiskScore,
+                attenuationDurations);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.US,
+                "ExposureInformation<"
+                        + "date: %s, "
+                        + "dateMillisSinceEpoch: %d, "
+                        + "durationMinutes: %d, "
+                        + "attenuationValue: %d, "
+                        + "transmissionRiskLevel: %d, "
+                        + "totalRiskScore: %d, "
+                        + "attenuationDurations: %s"
+                        + ">",
+                new Date(dateMillisSinceEpoch),
+                dateMillisSinceEpoch,
+                durationMinutes,
+                attenuationValue,
+                transmissionRiskLevel,
+                totalRiskScore,
+                Arrays.toString(attenuationDurations));
+    }
+
+    /** A builder for {@link ExposureInformation}. */
+    public static final class ExposureInformationBuilder {
+
+        private long dateMillisSinceEpoch = 0;
+        private int durationMinutes = 0;
+        private int attenuationValue = 0;
+        @RiskLevel private int transmissionRiskLevel = RiskLevel.RISK_LEVEL_INVALID;
+        private int totalRiskScore = 0;
+        private int[] attenuationDurations = new int[] {0, 0};
+
+        public ExposureInformationBuilder setDateMillisSinceEpoch(long dateMillisSinceEpoch) {
+            // This check only allows dates >= 1970-01-01, which is fairly arbitrary. Maybe we want to
+            // make these bounds a bit tighter?
+            checkArgument(
+                    dateMillisSinceEpoch >= 0,
+                    "dateMillisSinceEpoch (%s) must be >= 0",
+                    dateMillisSinceEpoch);
+            this.dateMillisSinceEpoch = dateMillisSinceEpoch;
+            return this;
+        }
+
+        public ExposureInformationBuilder setDurationMinutes(int durationMinutes) {
+            checkArgument(
+                    durationMinutes % 5 == 0,
+                    "durationMinutes (%s) must be an increment of 5",
+                    durationMinutes);
+            checkArgument(durationMinutes <= 30, "durationMinutes (%s) must be <= 30", durationMinutes);
+            this.durationMinutes = durationMinutes;
+            return this;
+        }
+
+        public ExposureInformationBuilder setAttenuationValue(int attenuationValue) {
+            checkArgument(
+                    attenuationValue >= 0 && attenuationValue <= 255,
+                    "attenuationValue (%s) must be >= 0 and <= 255",
+                    attenuationValue);
+            this.attenuationValue = attenuationValue;
+            return this;
+        }
+
+        public ExposureInformationBuilder setTransmissionRiskLevel(
+                @RiskLevel int transmissionRiskLevel) {
+            checkArgument(
+                    transmissionRiskLevel >= 0 && transmissionRiskLevel <= 8,
+                    "transmissionRiskLevel (%s) must be >= 0 and <= 8",
+                    transmissionRiskLevel);
+            this.transmissionRiskLevel = transmissionRiskLevel;
+            return this;
+        }
+
+        public ExposureInformationBuilder setTotalRiskScore(int totalRiskScore) {
+            checkArgument(
+                    totalRiskScore >= 0 && totalRiskScore <= 4096,
+                    "totalRiskScore (%s) must be >= 0 and <= 4096",
+                    totalRiskScore);
+            this.totalRiskScore = totalRiskScore;
+            return this;
+        }
+
+        public ExposureInformationBuilder setAttenuationDurations(int[] attenuationDurations) {
+            for (int attenuationDuration : attenuationDurations) {
+                checkArgument(
+                        attenuationDuration >= 0, "attenuationDuration (%s) must be >= 0", attenuationDuration);
+            }
+            this.attenuationDurations = Arrays.copyOf(attenuationDurations, attenuationDurations.length);
+            return this;
+        }
+
+        public ExposureInformation build() {
+            return new ExposureInformation(
+                    dateMillisSinceEpoch,
+                    durationMinutes,
+                    attenuationValue,
+                    transmissionRiskLevel,
+                    totalRiskScore,
+                    attenuationDurations);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationClient.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationClient.java
new file mode 100644
index 0000000..dafff49
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationClient.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import com.google.android.play.core.tasks.Task;
+
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+
+/** Interface for contact tracing APIs. */
+public interface ExposureNotificationClient {
+
+    /**
+     * Action which will be invoked via a BroadcastReceiver as a callback when the user has an updated
+     * exposure status. Also see {@link #EXTRA_EXPOSURE_SUMMARY} and {@link #EXTRA_TOKEN}, which will
+     * be included in this broadcast.
+     */
+    String ACTION_EXPOSURE_STATE_UPDATED =
+            "com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATED";
+
+    /**
+     * Action which will be invoked via a BroadcastReceiver as a callback when matching has finished
+     * and no matches were found. Also see {@link #EXTRA_TOKEN}, which will be included in this
+     * broadcast.
+     */
+    String ACTION_EXPOSURE_NOT_FOUND =
+            "com.google.android.gms.exposurenotification.ACTION_EXPOSURE_NOT_FOUND";
+
+    /**
+     * Extra attached to the {@link #ACTION_EXPOSURE_STATE_UPDATED} broadcast, giving a summary of the
+     * exposure details detected. Also see {@link #getExposureSummary}.
+     *
+     * @deprecated {@link ExposureSummary} is no longer provided when using the {@link
+     *     #getExposureWindows} API. Instead, use {@link #getDailySummaries}.
+     */
+    @Deprecated
+    String EXTRA_EXPOSURE_SUMMARY =
+            "com.google.android.gms.exposurenotification.EXTRA_EXPOSURE_SUMMARY";
+
+    /**
+     * Extra attached to the {@link #ACTION_EXPOSURE_STATE_UPDATED} broadcast, providing the token
+     * associated with the {@link #provideDiagnosisKeys} request.
+     *
+     * @deprecated Tokens are no longer used. Instead, prefer using the tokenless versions of {@link
+     *     #provideDiagnosisKeys}, {@link #getExposureWindows}, and {@link #getDailySummaries}.
+     */
+    @Deprecated String EXTRA_TOKEN = "com.google.android.gms.exposurenotification.EXTRA_TOKEN";
+
+    /** Activity action which shows the exposure notification settings screen. */
+    String ACTION_EXPOSURE_NOTIFICATION_SETTINGS =
+            "com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS";
+
+    /**
+     * Action which will be invoked via a BroadcastReceiver when the user modifies the state of
+     * exposure notifications via the Google Settings page. {@link #EXTRA_SERVICE_STATE} will be
+     * included as part of this broadcast.
+     */
+    String ACTION_SERVICE_STATE_UPDATED =
+            "com.google.android.gms.exposurenotification.ACTION_SERVICE_STATE_UPDATED";
+
+    /**
+     * Boolean extra attached to the {@link #ACTION_SERVICE_STATE_UPDATED} broadcast signifying
+     * whether the service is enabled or disabled.
+     */
+    String EXTRA_SERVICE_STATE = "com.google.android.gms.exposurenotification.EXTRA_SERVICE_STATE";
+
+    /**
+     * Token to be used with ExposureWindows API. Must be used with {@link #provideDiagnosisKeys}
+     * request when later using {@link #getExposureWindows}.
+     *
+     * @deprecated Tokens are no longer used. Instead, prefer using the tokenless versions of {@link
+     *     #provideDiagnosisKeys}, {@link #getExposureWindows}, and {@link #getDailySummaries}.
+     */
+    @Deprecated String TOKEN_A = "TYZWQ32170AXEUVCDW7A";
+
+    /**
+     * Settings.Global key for indicating whether a device supports locationless scanning for Exposure
+     * Notifications.
+     *
+     * @hide
+     */
+    public static final String LOCATIONLESS_SCANNING_SUPPORTED_SETTING =
+            "bluetooth_sanitized_exposure_notification_supported";
+
+    /**
+     * Starts BLE broadcasts and scanning based on the defined protocol.
+     *
+     * <p>If not previously started, this shows a user dialog for consent to start exposure detection
+     * and get permission.
+     *
+     * <p>Callbacks regarding exposure status will be provided via a BroadcastReceiver. Clients should
+     * register a receiver in their AndroidManifest which can handle the following action:
+     *
+     * <ul>
+     *   <li><code>com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATED</code>
+     * </ul>
+     *
+     * This receiver should also be guarded by the <code>
+     * com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK</code> permission so that
+     * other apps are not able to fake this broadcast.
+     */
+    Task<Void> start();
+
+    /**
+     * Starts BLE broadcasts and scanning for a given app.
+     *
+     * <p>Same as {@link #start()}, but could be called by a 1P client on behalf of the app. For
+     * example, this could be useful for Play Store.
+     *
+     * @param packageName the package of the app about which the requester inquires
+     * @param appName the human readable name of that app to be displayed in the resulted UI
+     * @param signatureHash the signature of that app
+     * @hide
+     */
+    Task<Void> startForPackage(String packageName, String appName, byte[] signatureHash);
+
+    /**
+     * Starts BLE broadcasts and scanning for a given app.
+     *
+     * <p>Same as {@link #start()}, but could be called by a 1P client on behalf of the app. For
+     * example, this could be useful for Play Store.
+     *
+     * @param packageName the package of the app about which the requester inquires
+     * @param appName the human readable name of that app to be displayed in the resulted UI
+     * @param signatureHash the signature of that app
+     * @param skipOptInDialog whether or not the opt in dialog should be skipped. This dialog should
+     *     only be skipped in the case where the caller has displayed the appropriate approval screens
+     *     to the user already and have gotten opt in from the user.
+     * @hide
+     */
+    Task<Void> startForPackage(
+            String packageName, String appName, byte[] signatureHash, boolean skipOptInDialog);
+
+    /**
+     * Starts BLE broadcasts and scanning for a given app.
+     *
+     * <p>Same as {@link #start()}, but could be called by a 1P client on behalf of the app. For
+     * example, this could be useful for Play Store.
+     *
+     * @param packageName the package of the app about which the requester inquires
+     * @param appName the human readable name of that app to be displayed in the resulted UI
+     * @param signatureHash the signature of that app
+     * @param skipOptInDialog whether or not the opt in dialog should be skipped. This dialog should
+     *     only be skipped in the case where the caller has displayed the appropriate approval screens
+     *     to the user already and have gotten opt in from the user.
+     * @param configuration settings which may be passed onto the app after it has finished starting.
+     * @hide
+     */
+    Task<Void> startForPackage(
+            String packageName,
+            String appName,
+            byte[] signatureHash,
+            boolean skipOptInDialog,
+            PackageConfiguration configuration);
+
+    /**
+     * Disables advertising and scanning. Contents of the database and keys will remain.
+     *
+     * <p>If the client app has been uninstalled by the user, this will be automatically invoked and
+     * the database and keys will be wiped from the device.
+     */
+    Task<Void> stop();
+
+    /** Indicates whether contact tracing is currently running for the requesting app. */
+    Task<Boolean> isEnabled();
+
+    /**
+     * Indicates whether contact tracing is currently running for a given app.
+     *
+     * <p>For use by 1P clients only.
+     *
+     * @param packageName the package of the app about which the requester inquires
+     * @param signatureHash the signature of that app
+     * @hide
+     */
+    Task<Boolean> isEnabledForPackage(String packageName, byte[] signatureHash);
+
+    /**
+     * Gets {@link TemporaryExposureKey} history to be stored on the server.
+     *
+     * <p>This should only be done after proper verification is performed on the client side that the
+     * user is diagnosed positive. Each key returned will have an unknown transmission risk level,
+     * clients should choose an appropriate risk level for these keys before uploading them to the
+     * server.
+     *
+     * <p>The keys provided here will only be from previous days; keys will not be released until
+     * after they are no longer an active exposure key.
+     *
+     * <p>This shows a user permission dialog for sharing and uploading data to the server.
+     */
+    Task<List<TemporaryExposureKey>> getTemporaryExposureKeyHistory();
+
+    /**
+     * Provides a list of diagnosis key files for exposure checking. The files are to be synced from
+     * the server. Old diagnosis keys (for example older than 14 days), will be ignored.
+     *
+     * <p>Diagnosis keys will be stored and matching will be performed in the near future, after which
+     * you’ll receive a broadcast with the {@link #ACTION_EXPOSURE_STATE_UPDATED} action. If no
+     * matches are found, you'll receive an {@link #ACTION_EXPOSURE_NOT_FOUND} action.
+     *
+     * <p>The diagnosis key files must be signed appropriately. Exposure configuration options can be
+     * provided to tune the matching algorithm. A unique token for this batch can also be provided,
+     * which will be used to associate the matches with this request as part of {@link
+     * #getExposureSummary} and {@link #getExposureInformation}. Alternatively, the same token can be
+     * passed in multiple times to concatenate results.
+     *
+     * <p>After the result Task has returned, keyFiles can be deleted.
+     *
+     * <p>Results for a given token remain for 14 days.
+     *
+     * @deprecated Tokens and configuration are no longer used. Instead, prefer using the tokenless,
+     *     configuration-less version of {@link #provideDiagnosisKeys}.
+     */
+    @Deprecated
+    Task<Void> provideDiagnosisKeys(
+            List<File> keyFiles, ExposureConfiguration configuration, String token);
+
+    /**
+     * Provides a list of diagnosis key files for exposure checking. The files are to be synced from
+     * the server. Old diagnosis keys (for example older than 14 days), will be ignored.
+     *
+     * <p>Diagnosis keys will be stored and matching will be performed in the near future, after which
+     * you’ll receive a broadcast with the {@link #ACTION_EXPOSURE_STATE_UPDATED} action. If no
+     * matches are found, you'll receive an {@link #ACTION_EXPOSURE_NOT_FOUND} action.
+     *
+     * <p>The diagnosis key files must be signed appropriately. Results from this request can also be
+     * queried at any time via {@link #getExposureWindows} and {@link #getDailySummaries}.
+     *
+     * <p>After the result Task has returned, keyFiles can be deleted.
+     *
+     * <p>Results remain for 14 days.
+     */
+    Task<Void> provideDiagnosisKeys(List<File> keyFiles);
+
+    /**
+     * Provides diagnosis key files for exposure checking. The files are to be synced from the server.
+     * Old diagnosis keys (for example older than 14 days), will be ignored.
+     *
+     * <p>Diagnosis keys will be stored and matching will be performed in the near future, after which
+     * you’ll receive a broadcast with the {@link #ACTION_EXPOSURE_STATE_UPDATED} action. If no
+     * matches are found, you'll receive an {@link #ACTION_EXPOSURE_NOT_FOUND} action.
+     *
+     * <p>The diagnosis key files must be signed appropriately. Results from this request can also be
+     * queried at any time via {@link #getExposureWindows} and {@link #getDailySummaries}.
+     *
+     * <p>After the result Task has returned, files can be deleted.
+     *
+     * <p>Results remain for 14 days.
+     *
+     * <p>This method is identical to providing a list of key files, but skips checking the EN version
+     * and assumes the provider can be used.
+     */
+    Task<Void> provideDiagnosisKeys(DiagnosisKeyFileProvider provider);
+
+    /**
+     * Retrieves the list of exposure windows corresponding to the TEKs given to provideKeys with
+     * token=TOKEN_A.
+     *
+     * <p>Long exposures to one TEK are split into windows of up to 30 minutes of scans, so a given
+     * TEK may lead to several exposure windows if beacon sightings for it spanned more than 30
+     * minutes. The link between them (the fact that they all correspond to the same TEK) is lost
+     * because those windows are shuffled before being returned and the underlying TEKs are not
+     * exposed by the API.
+     *
+     * <p>The provided token must be TOKEN_A.
+     *
+     * @deprecated Tokens are no longer used. Instead, prefer using the tokenless version of {@link
+     *     #getExposureWindows}.
+     */
+    @Deprecated
+    Task<List<ExposureWindow>> getExposureWindows(String token);
+
+    /**
+     * Retrieves the list of exposure windows corresponding to the TEKs given to {@link
+     * #provideDiagnosisKeys}.
+     *
+     * <p>Long exposures to one TEK are split into windows of up to 30 minutes of scans, so a given
+     * TEK may lead to several exposure windows if beacon sightings for it spanned more than 30
+     * minutes. The link between them (the fact that they all correspond to the same TEK) is lost
+     * because those windows are shuffled before being returned and the underlying TEKs are not
+     * exposed by the API.
+     */
+    Task<List<ExposureWindow>> getExposureWindows();
+
+    /**
+     * Gets a summary of the exposure calculation for the token, which should match the token provided
+     * in {@link #provideDiagnosisKeys}.
+     *
+     * @deprecated When using the {@link ExposureWindow} API, use {@link #getDailySummaries} instead.
+     */
+    @Deprecated
+    Task<ExposureSummary> getExposureSummary(String token);
+
+    /**
+     * Gets detailed information about exposures that have occurred related to the provided token,
+     * which should match the token provided in {@link #provideDiagnosisKeys}.
+     *
+     * <p>When multiple {@link ExposureInformation} objects are returned, they can be:
+     *
+     * <ul>
+     *   <li>Multiple encounters with a single diagnosis key.
+     *   <li>Multiple encounters with the same device across key rotation boundaries.
+     *   <li>Encounters with multiple devices.
+     * </ul>
+     *
+     * Records of calls to this method will be retained and viewable by the user.
+     *
+     * @deprecated When using the {@link ExposureWindow} API, use {@link #getExposureWindows} instead.
+     */
+    @Deprecated
+    Task<List<ExposureInformation>> getExposureInformation(String token);
+
+    /** Gets the current Exposure Notification version. */
+    Task<Long> getVersion();
+
+    /** Gets {@link CalibrationConfidence} of the current device. */
+    Task<Integer> getCalibrationConfidence();
+
+    /**
+     * Retrieves the per-day exposure summaries associated with the provided configuration.
+     *
+     * <p>A valid configuration must be provided to compute the summaries.
+     */
+    Task<List<DailySummary>> getDailySummaries(DailySummariesConfig dailySummariesConfig);
+
+    /**
+     * Sets the diagnosis keys data mapping if it wasn't already changed recently.
+     *
+     * <p>If called twice within 7 days, the second call will have no effect and will raise an
+     * exception with status code FAILED_RATE_LIMITED.
+     */
+    Task<Void> setDiagnosisKeysDataMapping(DiagnosisKeysDataMapping diagnosisKeysMetadataMapping);
+
+    /** Retrieves the current {@link DiagnosisKeysDataMapping}. */
+    Task<DiagnosisKeysDataMapping> getDiagnosisKeysDataMapping();
+
+    /**
+     * Checks whether the device supports Exposure Notification BLE scanning without requiring
+     * location to be enabled first.
+     */
+    boolean deviceSupportsLocationlessScanning();
+
+    /** Gets the current Exposure Notification status. */
+    Task<Set<ExposureNotificationStatus>> getStatus();
+
+    /**
+     * Retrieves the associated {@link PackageConfiguration} for the calling package. Note that this
+     * value can be null if no configuration was when starting.
+     */
+    Task<PackageConfiguration> getPackageConfiguration();
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationStatus.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationStatus.java
new file mode 100644
index 0000000..b114210
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationStatus.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/** Detail status for exposure notification service. */
+public enum ExposureNotificationStatus {
+    /** Exposure notification is running. */
+    ACTIVATED(0x00000001),
+    /** Exposure notification is not running. */
+    INACTIVATED(0x00000002),
+    /** Bluetooth is not enabled. */
+    BLUETOOTH_DISABLED(0x00000004),
+    /** Location is not enabled. */
+    LOCATION_DISABLED(0x00000008),
+    /** User is not consent for the client. */
+    NO_CONSENT(0x00000010),
+    /** The client is not in approved client list. */
+    NOT_IN_WHITELIST(0x00000020),
+    /** Can't detected the BLE supporting of this device due to bluetooth is not enabled. */
+    BLUETOOTH_SUPPORT_UNKNOWN(0x00000040),
+    /** Hardware of this device doesn't support exposure notification. */
+    HW_NOT_SUPPORT(0x00000080),
+    /** There is another client running as active client. */
+    FOCUS_LOST(0x00000100),
+    /** Device storage is not sufficient for exposure notification. */
+    LOW_STORAGE(0x00000200),
+    /** Current status is unknown. */
+    UNKNOWN(0x00000400),
+    /** Exposure notification is not supported. */
+    EN_NOT_SUPPORT(0x00000800),
+    /** Exposure notification is not supported for current user profile. */
+    USER_PROFILE_NOT_SUPPORT(0x00001000);
+
+    private final long value;
+
+    ExposureNotificationStatus(long value) {
+        this.value = value;
+    }
+
+    public static long getValues(Set<ExposureNotificationStatus> statusSet) {
+        long result = 0;
+        for (ExposureNotificationStatus status : statusSet) {
+            result |= status.value;
+        }
+        return result;
+    }
+
+    public static ImmutableSet<ExposureNotificationStatus> fromValue(long value) {
+        ImmutableSet.Builder<ExposureNotificationStatus> builder = ImmutableSet.builder();
+        for (ExposureNotificationStatus status : ExposureNotificationStatus.values()) {
+            if ((status.value & value) != 0) {
+                builder.add(status);
+            }
+        }
+        return builder.build();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationStatusCodes.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationStatusCodes.java
new file mode 100644
index 0000000..1eb6176
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureNotificationStatusCodes.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+/**
+ * Status codes for nearby contact tracing results.
+ */
+public final class ExposureNotificationStatusCodes {
+
+    private ExposureNotificationStatusCodes() {
+    }
+
+    // IMPORTANT NOTE: The codes referenced in this file are used on both the client and service
+    // side, and must not be modified after launch. It is fine to add new codes, but previously
+    // existing codes must be left unmodified.
+
+    // Common status codes that may be used by a variety of actions.
+
+    /**
+     * The operation was successful.
+     */
+    public static final int SUCCESS = 0;
+
+    /**
+     * An internal error occurred. Retrying should resolve the problem.
+     */
+    public static final int INTERNAL_ERROR = 8;
+
+    /**
+     * The application is misconfigured. This error is not recoverable and will be treated as fatal.
+     * The developer should look at the logs after this to determine more actionable information.
+     */
+    public static final int DEVELOPER_ERROR = 10;
+
+    /**
+     * The operation failed, without any more information.
+     */
+    public static final int FAILED = 13;
+
+    /**
+     * A blocking call was interrupted while waiting and did not run to completion.
+     */
+    public static final int INTERRUPTED = 14;
+
+    /**
+     * Timed out while awaiting the result.
+     */
+    public static final int TIMEOUT = 15;
+
+    /**
+     * The result was canceled.
+     */
+    public static final int CANCELED = 16;
+
+    /**
+     * The app was already in the requested state so the call did nothing.
+     */
+    public static final int FAILED_ALREADY_STARTED = 39500;
+
+    /**
+     * The hardware capability of the device was not supported.
+     */
+    public static final int FAILED_NOT_SUPPORTED = 39501;
+
+    /**
+     * The user rejected the opt-in state.
+     */
+    public static final int FAILED_REJECTED_OPT_IN = 39502;
+
+    /**
+     * The functionality was disabled by the user or the phone.
+     */
+    public static final int FAILED_SERVICE_DISABLED = 39503;
+
+    /**
+     * The bluetooth was powered off.
+     */
+    public static final int FAILED_BLUETOOTH_DISABLED = 39504;
+
+    /**
+     * The service was disabled for some reasons temporarily.
+     */
+    public static final int FAILED_TEMPORARILY_DISABLED = 39505;
+
+    /**
+     * The operation failed during a disk read/write.
+     */
+    public static final int FAILED_DISK_IO = 39506;
+
+    /**
+     * The client is unauthorized to access the APIs.
+     */
+    public static final int FAILED_UNAUTHORIZED = 39507;
+
+    /**
+     * The client has been rate limited for access to this API.
+     */
+    public static final int FAILED_RATE_LIMITED = 39508;
+
+    /**
+     * Returns an untranslated debug (not user-friendly!) string based on the current status code.
+     */
+    public static String getStatusCodeString(int statusCode) {
+        switch (statusCode) {
+            case FAILED_ALREADY_STARTED:
+                return "FAILED_ALREADY_STARTED";
+            case FAILED_NOT_SUPPORTED:
+                return "FAILED_NOT_SUPPORTED";
+            case FAILED_REJECTED_OPT_IN:
+                return "FAILED_REJECTED_OPT_IN";
+            case FAILED_SERVICE_DISABLED:
+                return "FAILED_SERVICE_DISABLED";
+            case FAILED_BLUETOOTH_DISABLED:
+                return "FAILED_BLUETOOTH_DISABLED";
+            case FAILED_TEMPORARILY_DISABLED:
+                return "FAILED_TEMPORARILY_DISABLED";
+            case FAILED_DISK_IO:
+                return "FAILED_DISK_IO";
+            case FAILED_UNAUTHORIZED:
+                return "FAILED_UNAUTHORIZED";
+            case FAILED_RATE_LIMITED:
+                return "FAILED_RATE_LIMITED";
+            default:
+                return "unknown status code: " + statusCode;
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureSummary.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureSummary.java
new file mode 100644
index 0000000..680ee33
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureSummary.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
+import com.google.samples.exposurenotification.safeparcel.ReflectedParcelable;
+import com.google.samples.exposurenotification.safeparcel.SafeParcelable;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Summary information about recent exposures.
+ *
+ * <p>The client can get this information via {@link ExposureNotificationClient#getExposureSummary}.
+ *
+ * @deprecated no longer used with Exposure Window API.
+ */
+@SafeParcelable.Class(creator = "ExposureSummaryCreator")
+@Deprecated
+public final class ExposureSummary extends AbstractSafeParcelable implements ReflectedParcelable {
+
+    @Field(id = 1, getter = "getDaysSinceLastExposure")
+    int daysSinceLastExposure;
+
+    @Field(id = 2, getter = "getMatchedKeyCount")
+    int matchedKeyCount;
+
+    @Field(id = 3, getter = "getMaximumRiskScore")
+    int maximumRiskScore;
+
+    @Field(id = 4, getter = "getAttenuationDurationsInMinutes")
+    int[] attenuationDurations;
+
+    @Field(id = 5, getter = "getSummationRiskScore")
+    int summationRiskScore;
+
+    @Constructor
+    ExposureSummary(
+            @Param(id = 1) int daysSinceLastExposure,
+            @Param(id = 2) int matchedKeyCount,
+            @Param(id = 3) int maximumRiskScore,
+            @Param(id = 4) int[] attenuationDurations,
+            @Param(id = 5) int summationRiskScore) {
+        this.daysSinceLastExposure = daysSinceLastExposure;
+        this.matchedKeyCount = matchedKeyCount;
+        this.maximumRiskScore = maximumRiskScore;
+        this.attenuationDurations = attenuationDurations;
+        this.summationRiskScore = summationRiskScore;
+    }
+
+    /**
+     * Days since last match to a diagnosis key from the server. 0 is today, 1 is yesterday, etc. Only
+     * valid if {@link #getMatchedKeyCount} > 0.
+     */
+    public int getDaysSinceLastExposure() {
+        return daysSinceLastExposure;
+    }
+
+    /** Number of matched diagnosis keys. */
+    public int getMatchedKeyCount() {
+        return matchedKeyCount;
+    }
+
+    /** The highest risk score of all exposure incidents, it will be a value 0-4096. */
+    public int getMaximumRiskScore() {
+        return maximumRiskScore;
+    }
+
+    /**
+     * Array of durations in minutes at certain radio signal attenuations. The thresholds are defined
+     * in {@link ExposureConfiguration#getDurationAtAttenuationThresholds}.
+     *
+     * <p>Array index 0: Sum of durations for all exposures when attenuation < low threshold.
+     *
+     * <p>Array index 1: Sum of durations for all exposures when low threshold <= attenuation < high
+     * threshold.
+     *
+     * <p>Array index 2: Sum of durations for all exposures when attenuation >= high threshold.
+     *
+     * <p>These durations are aggregated across all exposures and capped at 30 minutes.
+     */
+    public int[] getAttenuationDurationsInMinutes() {
+        return Arrays.copyOf(attenuationDurations, attenuationDurations.length);
+    }
+
+    /** The summation of risk scores of all exposure incidents. */
+    public int getSummationRiskScore() {
+        return summationRiskScore;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof ExposureSummary) {
+            ExposureSummary that = (ExposureSummary) obj;
+            return Objects.equal(daysSinceLastExposure, that.getDaysSinceLastExposure())
+                    && Objects.equal(matchedKeyCount, that.getMatchedKeyCount())
+                    && Objects.equal(maximumRiskScore, that.getMaximumRiskScore())
+                    && Arrays.equals(attenuationDurations, that.getAttenuationDurationsInMinutes())
+                    && Objects.equal(summationRiskScore, that.getSummationRiskScore());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(
+                daysSinceLastExposure,
+                matchedKeyCount,
+                maximumRiskScore,
+                attenuationDurations,
+                summationRiskScore);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.US,
+                "ExposureSummary<"
+                        + "daysSinceLastExposure: %d, "
+                        + "matchedKeyCount: %d, "
+                        + "maximumRiskScore: %d, "
+                        + "attenuationDurations: %s, "
+                        + "summationRiskScore: %d"
+                        + ">",
+                daysSinceLastExposure,
+                matchedKeyCount,
+                maximumRiskScore,
+                Arrays.toString(attenuationDurations),
+                summationRiskScore);
+    }
+
+    /** A builder for {@link ExposureSummary}. */
+    public static final class ExposureSummaryBuilder {
+
+        private int daysSinceLastExposure = 0;
+        private int matchedKeyCount = 0;
+        private int maximumRiskScore = 0;
+        private int[] attenuationDurations = new int[] {0, 0, 0};
+        private int summationRiskScore = 0;
+
+        public ExposureSummaryBuilder setDaysSinceLastExposure(int daysSinceLastExposure) {
+            checkArgument(
+                    daysSinceLastExposure >= 0,
+                    "daysSinceLastExposure (%s) must be >= 0",
+                    daysSinceLastExposure);
+            this.daysSinceLastExposure = daysSinceLastExposure;
+            return this;
+        }
+
+        public ExposureSummaryBuilder setMatchedKeyCount(int matchedKeyCount) {
+            checkArgument(matchedKeyCount >= 0, "matchedKeyCount (%s) must be >= 0", matchedKeyCount);
+            this.matchedKeyCount = matchedKeyCount;
+            return this;
+        }
+
+        public ExposureSummaryBuilder setMaximumRiskScore(int maximumRiskScore) {
+            checkArgument(
+                    maximumRiskScore >= 0 && maximumRiskScore <= 4096,
+                    "maximumRiskScore (%s) must be >= 0 and <= 4096",
+                    maximumRiskScore);
+            this.maximumRiskScore = maximumRiskScore;
+            return this;
+        }
+
+        public ExposureSummaryBuilder setAttenuationDurations(int[] attenuationDurations) {
+            checkArgument(attenuationDurations.length == 3);
+            for (int attenuationDuration : attenuationDurations) {
+                checkArgument(
+                        attenuationDuration >= 0, "attenuationDuration (%s) must be >= 0", attenuationDuration);
+            }
+            this.attenuationDurations = Arrays.copyOf(attenuationDurations, attenuationDurations.length);
+            return this;
+        }
+
+        public ExposureSummaryBuilder setSummationRiskScore(int summationRiskScore) {
+            checkArgument(
+                    summationRiskScore >= 0, "summationRiskScore (%s) must be >= 0", summationRiskScore);
+            this.summationRiskScore = summationRiskScore;
+            return this;
+        }
+
+        public ExposureSummary build() {
+            return new ExposureSummary(
+                    daysSinceLastExposure,
+                    matchedKeyCount,
+                    maximumRiskScore,
+                    attenuationDurations,
+                    summationRiskScore);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureWindow.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureWindow.java
new file mode 100644
index 0000000..87ac3ee
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ExposureWindow.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.samples.exposurenotification.ExposureNotificationEnums;
+
+import java.util.List;
+import java.util.Locale;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A duration of up to 30 minutes during which beacons from a TEK were observed.
+ *
+ * <p>Each ExposureWindow corresponds to a single TEK, but one TEK can lead to several
+ * ExposureWindow due to random 15-30 minutes cuts. See getExposureWindows() for more info.
+ *
+ * <p>The TEK itself isn't exposed by the API.
+ */
+public final class ExposureWindow {
+
+    long dateMillisSinceEpoch;
+
+    final List<ScanInstance> scanInstances;
+    @ReportType
+    final int reportType;
+
+    @Infectiousness
+    final int infectiousness;
+
+    @CalibrationConfidence
+    final int calibrationConfidence;
+
+    ExposureWindow(
+            long dateMillisSinceEpoch,
+            List<ScanInstance> scanInstances,
+            @ReportType int reportType,
+            @Infectiousness int infectiousness,
+            @CalibrationConfidence int calibrationConfidence) {
+        this.dateMillisSinceEpoch = dateMillisSinceEpoch;
+        this.scanInstances = ImmutableList.copyOf(scanInstances);
+        this.reportType = reportType;
+        this.infectiousness = infectiousness;
+        this.calibrationConfidence = calibrationConfidence;
+    }
+
+    /**
+     * Returns the epoch time in milliseconds the exposure occurred. This will represent the start of
+     * a day in UTC.
+     */
+    public long getDateMillisSinceEpoch() {
+        return dateMillisSinceEpoch;
+    }
+
+    /**
+     * Sightings of this ExposureWindow, time-ordered.
+     *
+     * <p>Each sighting corresponds to a scan (of a few seconds) during which a beacon with the TEK
+     * causing this exposure was observed.
+     */
+    public List<ScanInstance> getScanInstances() {
+        return scanInstances;
+    }
+
+    /**
+     * Report Type of the TEK that caused this exposure
+     *
+     * <p>TEKs with no report type set are returned with reportType=CONFIRMED_TEST.
+     *
+     * <p>TEKs with RECURSIVE report type may be dropped because this report type is reserved for
+     * future use.
+     *
+     * <p>TEKs with REVOKED or invalid report types do not lead to exposures.
+     */
+    @ReportType
+    public int getReportType() {
+        return reportType;
+    }
+
+    /**
+     * Infectiousness of the TEK that caused this exposure, computed from the days since onset of
+     * symptoms using the daysToInfectiousnessMapping.
+     */
+    @Infectiousness
+    public int getInfectiousness() {
+        return infectiousness;
+    }
+
+    /**
+     * Confidence of the BLE Transmit power calibration of the transmitting device.
+     */
+    @CalibrationConfidence
+    public int getCalibrationConfidence() {
+        return calibrationConfidence;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        ExposureWindow that = (ExposureWindow) o;
+        return reportType == that.reportType
+                && dateMillisSinceEpoch == that.dateMillisSinceEpoch
+                && scanInstances.equals(that.scanInstances)
+                && infectiousness == that.infectiousness
+                && calibrationConfidence == that.calibrationConfidence;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(
+                dateMillisSinceEpoch, scanInstances, reportType, infectiousness, calibrationConfidence);
+    }
+
+    @Override
+    public String toString() {
+        return "ExposureWindow{"
+                + "dateMillisSinceEpoch="
+                + dateMillisSinceEpoch
+                + ", reportType="
+                + reportType
+                + ", scanInstances="
+                + scanInstances
+                + ", infectiousness="
+                + infectiousness
+                + ", calibrationConfidence="
+                + calibrationConfidence
+                + '}';
+    }
+
+    /**
+     * Builder for ExposureWindow.
+     */
+    public static class Builder {
+
+        private long dateMillisSinceEpoch;
+        private List<ScanInstance> scanInstances;
+        @ReportType
+        private int reportType;
+        @Infectiousness
+        private int infectiousness;
+        @CalibrationConfidence
+        private int calibrationConfidence;
+
+        public Builder() {
+            dateMillisSinceEpoch = 0;
+            scanInstances = ImmutableList.of();
+            reportType = ReportType.CONFIRMED_TEST;
+            infectiousness = Infectiousness.STANDARD;
+            calibrationConfidence = CalibrationConfidence.LOWEST;
+        }
+
+        public Builder setDateMillisSinceEpoch(long dateMillisSinceEpoch) {
+            this.dateMillisSinceEpoch = dateMillisSinceEpoch;
+            return this;
+        }
+
+        public Builder setScanInstances(List<ScanInstance> scanInstances) {
+            this.scanInstances = Preconditions.checkNotNull(scanInstances);
+            return this;
+        }
+
+        public Builder setReportType(@ReportType int reportType) {
+            checkArgument(
+                    reportType > ReportType.UNKNOWN && reportType < ReportType.REVOKED,
+                    "reportType (%d) is not allowed",
+                    reportType);
+            this.reportType = reportType;
+            return this;
+        }
+
+        public Builder setInfectiousness(@Infectiousness int infectiousness) {
+            Preconditions.checkNotNull(
+                    ExposureNotificationEnums.Infectiousness.forNumber(infectiousness),
+                    String.format(Locale.getDefault(), "infectiousness (%d) is invalid", infectiousness));
+            this.infectiousness = infectiousness;
+            return this;
+        }
+
+        public Builder setCalibrationConfidence(@CalibrationConfidence int calibrationConfidence) {
+            checkNotNull(
+                    ExposureNotificationEnums.CalibrationConfidence.forNumber(calibrationConfidence),
+                    String.format(
+                            Locale.getDefault(), "calibrationConfidence (%d) is invalid", calibrationConfidence));
+            this.calibrationConfidence = calibrationConfidence;
+            return this;
+        }
+
+        public ExposureWindow build() {
+            return new ExposureWindow(
+                    dateMillisSinceEpoch, scanInstances, reportType, infectiousness, calibrationConfidence);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/Infectiousness.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/Infectiousness.java
new file mode 100644
index 0000000..84c413b
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/Infectiousness.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.IntDef;
+
+import com.google.samples.exposurenotification.ExposureNotificationEnums;
+
+/**
+ * Infectiousness defined for an {@link ExposureWindow}.
+ */
+@IntDef({
+        Infectiousness.NONE,
+        Infectiousness.STANDARD,
+        Infectiousness.HIGH,
+})
+public @interface Infectiousness {
+    int NONE = ExposureNotificationEnums.Infectiousness.INFECTIOUSNESS_NONE_VALUE;
+    int STANDARD = ExposureNotificationEnums.Infectiousness.INFECTIOUSNESS_STANDARD_VALUE;
+    int HIGH = ExposureNotificationEnums.Infectiousness.INFECTIOUSNESS_HIGH_VALUE;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/PackageConfiguration.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/PackageConfiguration.java
new file mode 100644
index 0000000..b082f37
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/PackageConfiguration.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
+import com.google.samples.exposurenotification.safeparcel.SafeParcelable;
+
+import java.util.Locale;
+
+/**
+ * Holds configuration values that can be passed onto the client app after it has finished
+ * installing via {@link ExposureNotificationClient#getPackageConfiguration()}.
+ */
+@SafeParcelable.Class(creator = "PackageConfigurationCreator")
+public final class PackageConfiguration extends AbstractSafeParcelable {
+
+    Bundle values;
+
+    @Constructor
+    PackageConfiguration(@Param(id = 1) Bundle values) {
+        this.values = values;
+    }
+
+    /** Returns a Bundle containing all configuration options. */
+    public Bundle getValues() {
+        return values;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof PackageConfiguration) {
+            PackageConfiguration that = (PackageConfiguration) obj;
+            return Objects.equal(values, that.getValues());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(values);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(Locale.US, "PackageConfiguration<values: %s>", values);
+    }
+
+    /** A builder for {@link PackageConfiguration}. */
+    public static final class PackageConfigurationBuilder {
+
+        private Bundle values;
+
+        /** Sets a Bundle containing configuration options. */
+        public PackageConfigurationBuilder setValues(Bundle values) {
+            this.values = new Bundle(values);
+            return this;
+        }
+
+        /** Builds a {@link PackageConfiguration}. */
+        public PackageConfiguration build() {
+            return new PackageConfiguration(values);
+        }
+    }
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ReportType.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ReportType.java
new file mode 100644
index 0000000..50bb8a7
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ReportType.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.IntDef;
+
+/**
+ * Report type defined for a {@link TemporaryExposureKey}.
+ */
+@IntDef({
+        ReportType.UNKNOWN,
+        ReportType.CONFIRMED_TEST,
+        ReportType.CONFIRMED_CLINICAL_DIAGNOSIS,
+        ReportType.SELF_REPORT,
+        ReportType.RECURSIVE,
+        ReportType.REVOKED,
+})
+public @interface ReportType {
+    int UNKNOWN = 0;
+    int CONFIRMED_TEST = 1;
+    int CONFIRMED_CLINICAL_DIAGNOSIS = 2;
+    int SELF_REPORT = 3;
+    int RECURSIVE = 4;
+    int REVOKED = 5;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/RiskLevel.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/RiskLevel.java
new file mode 100644
index 0000000..6bb83b3
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/RiskLevel.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Risk level defined for an {@link TemporaryExposureKey}.
+ */
+@IntDef({
+        RiskLevel.RISK_LEVEL_INVALID,
+        RiskLevel.RISK_LEVEL_LOWEST,
+        RiskLevel.RISK_LEVEL_LOW,
+        RiskLevel.RISK_LEVEL_LOW_MEDIUM,
+        RiskLevel.RISK_LEVEL_MEDIUM,
+        RiskLevel.RISK_LEVEL_MEDIUM_HIGH,
+        RiskLevel.RISK_LEVEL_HIGH,
+        RiskLevel.RISK_LEVEL_VERY_HIGH,
+        RiskLevel.RISK_LEVEL_HIGHEST,
+})
+@Retention(RetentionPolicy.SOURCE)
+public @interface RiskLevel {
+    int RISK_LEVEL_INVALID = 0;
+    int RISK_LEVEL_LOWEST = 1;
+    int RISK_LEVEL_LOW = 2;
+    int RISK_LEVEL_LOW_MEDIUM = 3;
+    int RISK_LEVEL_MEDIUM = 4;
+    int RISK_LEVEL_MEDIUM_HIGH = 5;
+    int RISK_LEVEL_HIGH = 6;
+    int RISK_LEVEL_VERY_HIGH = 7;
+    int RISK_LEVEL_HIGHEST = 8;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ScanInstance.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ScanInstance.java
new file mode 100644
index 0000000..217bf00
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ScanInstance.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+
+import com.google.common.base.Objects;
+
+/**
+ * Information about the sighting of a TEK within a BLE scan (of a few seconds).
+ *
+ * <p>The TEK itself isn't exposed by the API.
+ */
+public final class ScanInstance {
+
+    int typicalAttenuationDb;
+    int minAttenuationDb;
+    int secondsSinceLastScan;
+
+    ScanInstance(
+            int typicalAttenuationDb,
+            int minAttenuationDb,
+            int secondsSinceLastScan) {
+        this.typicalAttenuationDb = typicalAttenuationDb;
+        this.minAttenuationDb = minAttenuationDb;
+        this.secondsSinceLastScan = secondsSinceLastScan;
+    }
+
+    /**
+     * Aggregation of the attenuations of all of this TEK's beacons received during the scan, in dB.
+     * This is most likely to be an average in the dB domain.
+     */
+    public int getTypicalAttenuationDb() {
+        return typicalAttenuationDb;
+    }
+
+    /**
+     * Minimum attenuation of all of this TEK's beacons received during the scan, in dB.
+     */
+    public int getMinAttenuationDb() {
+        return minAttenuationDb;
+    }
+
+    /**
+     * Seconds elapsed since the previous scan, typically used as a weight.
+     *
+     * <p>Two example uses: - Summing those values over all sightings of an exposure provides the
+     * duration of that exposure. - Summing those values over all sightings in a given attenuation
+     * range and over all exposures recreates the durationAtBuckets of v1.
+     *
+     * <p>Note that the previous scan may not have led to a sighting of that TEK.
+     */
+    public int getSecondsSinceLastScan() {
+        return secondsSinceLastScan;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        ScanInstance that = (ScanInstance) o;
+        return typicalAttenuationDb == that.typicalAttenuationDb
+                && minAttenuationDb == that.minAttenuationDb
+                && secondsSinceLastScan == that.secondsSinceLastScan;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(typicalAttenuationDb, minAttenuationDb, secondsSinceLastScan);
+    }
+
+    @Override
+    public String toString() {
+        return "ScanInstance{"
+                + "typicalAttenuationDb="
+                + typicalAttenuationDb
+                + ", minAttenuationDb="
+                + minAttenuationDb
+                + ", secondsSinceLastScan="
+                + secondsSinceLastScan
+                + '}';
+    }
+
+    /**
+     * Builder for ScanInstance.
+     */
+    public static class Builder {
+
+        private int typicalAttenuationDb;
+        private int minAttenuationDb;
+        private int secondsSinceLastScan;
+
+        public Builder() {
+            typicalAttenuationDb = 0;
+            minAttenuationDb = 0;
+            secondsSinceLastScan = 0;
+        }
+
+        public Builder setTypicalAttenuationDb(int typicalAttenuationDb) {
+            this.typicalAttenuationDb = typicalAttenuationDb;
+            return this;
+        }
+
+        public Builder setMinAttenuationDb(int minAttenuationDb) {
+            this.minAttenuationDb = minAttenuationDb;
+            return this;
+        }
+
+        public Builder setSecondsSinceLastScan(int secondsSinceLastScan) {
+            this.secondsSinceLastScan = secondsSinceLastScan;
+            return this;
+        }
+
+        public ScanInstance build() {
+            return new ScanInstance(typicalAttenuationDb, minAttenuationDb, secondsSinceLastScan);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ScannedPacket.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ScannedPacket.java
new file mode 100644
index 0000000..cddf0da
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/ScannedPacket.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import androidx.annotation.Nullable;
+
+import com.google.common.base.Objects;
+import com.google.samples.Hex;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+/**
+ * A scanned packet coming from other device's EN advertising.
+ *
+ * @hide
+ */
+public class ScannedPacket {
+
+    byte[] id;
+    byte[] encryptedMetadata;
+    ScannedPacketContent[] scannedPacketContents;
+
+    ScannedPacket(
+            byte[] id,
+            byte[] encryptedMetadata,
+            ScannedPacketContent[] scannedPacketContents) {
+        this.id = id;
+        this.encryptedMetadata = encryptedMetadata;
+        this.scannedPacketContents = scannedPacketContents;
+    }
+
+    /**
+     * Returns the rolling proximity ID.
+     */
+    public byte[] getId() {
+        return Arrays.copyOf(id, id.length);
+    }
+
+    /**
+     * Returns the encrypted metadata.
+     */
+    public byte[] getEncryptedMetadata() {
+        return Arrays.copyOf(encryptedMetadata, encryptedMetadata.length);
+    }
+
+    /**
+     * Returns the scanned packet contents.
+     */
+    public ScannedPacketContent[] getScannedPacketContents() {
+        return Arrays.copyOf(scannedPacketContents, scannedPacketContents.length);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof ScannedPacket) {
+            ScannedPacket that = (ScannedPacket) obj;
+            return Arrays.equals(id, that.id)
+                    && Arrays.equals(encryptedMetadata, that.encryptedMetadata)
+                    && Arrays.equals(scannedPacketContents, that.scannedPacketContents);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(id, encryptedMetadata, scannedPacketContents);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder scannedPacketContentsString = new StringBuilder();
+        for (ScannedPacketContent scannedPacketContent : scannedPacketContents) {
+            scannedPacketContentsString.append(scannedPacketContent.toString()).append(",");
+        }
+        return String.format(
+                Locale.US,
+                "ScannedPacket<id: %s, encryptedMetadata: %s, scannedPacketContents: %s>",
+                Hex.bytesToStringLowercase(id),
+                Hex.bytesToStringLowercase(encryptedMetadata),
+                scannedPacketContentsString);
+    }
+
+    /**
+     * A builder for {@link ScannedPacket}.
+     */
+    public static final class ScannedPacketBuilder {
+        private byte[] id = new byte[0];
+        private byte[] encryptedMetadata = new byte[0];
+        private ScannedPacketContent[] scannedPacketContents = new ScannedPacketContent[0];
+
+        public ScannedPacketBuilder setId(byte[] id) {
+            this.id = Arrays.copyOf(id, id.length);
+            return this;
+        }
+
+        public ScannedPacketBuilder setEncryptedMetadata(byte[] encryptedMetadata) {
+            this.encryptedMetadata = Arrays.copyOf(encryptedMetadata, encryptedMetadata.length);
+            return this;
+        }
+
+        public ScannedPacketBuilder setScannedPacketContents(
+                ScannedPacketContent[] scannedPacketContents) {
+            checkArgument(
+                    scannedPacketContents != null && scannedPacketContents.length > 0,
+                    "scannedPacketContents's size must be > 0");
+            this.scannedPacketContents =
+                    Arrays.copyOf(scannedPacketContents, scannedPacketContents.length);
+            return this;
+        }
+
+        public ScannedPacket build() {
+            return new ScannedPacket(id, encryptedMetadata, scannedPacketContents);
+        }
+    }
+
+    /**
+     * A scanned packet content coming from other device's EN advertising.
+     *
+     * @hide
+     */
+    public static class ScannedPacketContent {
+
+        int epochSeconds;
+        int rssi;
+        int previousScanEpochSeconds;
+
+        ScannedPacketContent(
+                int epochSeconds,
+                int rssi,
+                int previousScanEpochSeconds) {
+            this.epochSeconds = epochSeconds;
+            this.rssi = rssi;
+            this.previousScanEpochSeconds = previousScanEpochSeconds;
+        }
+
+        public int getEpochSeconds() {
+            return epochSeconds;
+        }
+
+        public int getRssi() {
+            return rssi;
+        }
+
+        public int getPreviousScanEpochSeconds() {
+            return previousScanEpochSeconds;
+        }
+
+        @Override
+        public boolean equals(@Nullable Object obj) {
+            if (obj instanceof ScannedPacketContent) {
+                ScannedPacketContent that = (ScannedPacketContent) obj;
+                return Objects.equal(epochSeconds, that.epochSeconds)
+                        && Objects.equal(rssi, that.rssi)
+                        && Objects.equal(previousScanEpochSeconds, that.previousScanEpochSeconds);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(epochSeconds, rssi, previousScanEpochSeconds);
+        }
+
+        @Override
+        public String toString() {
+            return String.format(
+                    Locale.US,
+                    "ScannedPacketContent<"
+                            + "epochSeconds: %s, "
+                            + "rssi: %d, "
+                            + "previousScanEpochSeconds: %s>",
+                    new Date(SECONDS.toMillis(epochSeconds)),
+                    rssi,
+                    new Date(SECONDS.toMillis(previousScanEpochSeconds)));
+        }
+
+        /**
+         * A builder for {@link ScannedPacketContent}.
+         */
+        public static final class ScannedPacketContentBuilder {
+            private int epochSeconds = 0;
+            private int rssi = 0;
+            private int previousScanEpochSeconds = 0;
+
+            public ScannedPacketContentBuilder setEpochSeconds(int epochSeconds) {
+                this.epochSeconds = epochSeconds;
+                return this;
+            }
+
+            public ScannedPacketContentBuilder setRssi(int rssi) {
+                this.rssi = rssi;
+                return this;
+            }
+
+            public ScannedPacketContentBuilder setPreviousScanEpochSeconds(int previousScanEpochSeconds) {
+                this.previousScanEpochSeconds = previousScanEpochSeconds;
+                return this;
+            }
+
+            public ScannedPacketContent build() {
+                return new ScannedPacketContent(epochSeconds, rssi, previousScanEpochSeconds);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/TemporaryExposureKey.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/TemporaryExposureKey.java
new file mode 100644
index 0000000..e14ef50
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/TemporaryExposureKey.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+/**
+ * A key generated for advertising over a window of time.
+ */
+public final class TemporaryExposureKey implements Parcelable {
+    /**
+     * The default value for {@link #getDaysSinceOnsetOfSymptoms}.
+     *
+     * <p>See {@link DiagnosisKeysDataMapping#getDaysSinceOnsetToInfectiousness} for more information.
+     */
+    public static final int DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN = Integer.MAX_VALUE;
+
+    public static final Parcelable.Creator<TemporaryExposureKey> CREATOR =
+            new Parcelable.Creator<TemporaryExposureKey>() {
+
+                @Override
+                public TemporaryExposureKey createFromParcel(Parcel source) {
+                    return new TemporaryExposureKey(source);
+                }
+
+                @Override
+                public TemporaryExposureKey[] newArray(int size) {
+                    return new TemporaryExposureKey[size];
+                }
+            };
+
+    byte[] keyData;
+    int rollingStartIntervalNumber;
+    @RiskLevel
+    int transmissionRiskLevel;
+    int rollingPeriod;
+    @ReportType
+    int reportType;
+    int daysSinceOnsetOfSymptoms;
+
+    TemporaryExposureKey(
+            byte[] keyData,
+            int rollingStartIntervalNumber,
+            @RiskLevel int transmissionRiskLevel,
+            int rollingPeriod,
+            @ReportType int reportType,
+            int daysSinceOnsetOfSymptoms) {
+        this.keyData = keyData;
+        this.rollingStartIntervalNumber = rollingStartIntervalNumber;
+        this.transmissionRiskLevel = transmissionRiskLevel;
+        this.rollingPeriod = rollingPeriod;
+        this.reportType = reportType;
+        this.daysSinceOnsetOfSymptoms = daysSinceOnsetOfSymptoms;
+    }
+
+    TemporaryExposureKey(
+            byte[] keyData,
+            int rollingStartIntervalNumber,
+            @RiskLevel int transmissionRiskLevel,
+            int rollingPeriod,
+            int daysSinceOnsetOfSymptoms) {
+        this.keyData = keyData;
+        this.rollingStartIntervalNumber = rollingStartIntervalNumber;
+        this.transmissionRiskLevel = transmissionRiskLevel;
+        this.rollingPeriod = rollingPeriod;
+        this.daysSinceOnsetOfSymptoms = daysSinceOnsetOfSymptoms;
+    }
+
+    private TemporaryExposureKey(@NonNull Parcel in) {
+        int keyLen = in.readInt();
+        this.keyData = new byte[keyLen];
+        in.readByteArray(this.keyData);
+        this.rollingStartIntervalNumber = in.readInt();
+        this.transmissionRiskLevel = in.readInt();
+        this.rollingPeriod = in.readInt();
+        this.daysSinceOnsetOfSymptoms = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(this.keyData.length);
+        dest.writeByteArray(this.keyData);
+        dest.writeInt(this.rollingStartIntervalNumber);
+        dest.writeInt(this.transmissionRiskLevel);
+        dest.writeInt(this.rollingPeriod);
+        dest.writeInt(this.daysSinceOnsetOfSymptoms);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * The randomly generated Temporary Exposure Key information.
+     */
+    public byte[] getKeyData() {
+        return Arrays.copyOf(keyData, keyData.length);
+    }
+
+    /**
+     * A number describing when a key starts. It is equal to startTimeOfKeySinceEpochInSecs / (60 *
+     * 10).
+     */
+    public int getRollingStartIntervalNumber() {
+        return rollingStartIntervalNumber;
+    }
+
+    /**
+     * Risk of transmission associated with the person this key came from.
+     */
+    @RiskLevel
+    public int getTransmissionRiskLevel() {
+        return transmissionRiskLevel;
+    }
+
+    /**
+     * A number describing how long a key is valid. It is expressed in increments of 10 minutes (e.g.
+     * 144 for 24 hours).
+     */
+    public int getRollingPeriod() {
+        return rollingPeriod;
+    }
+
+    /**
+     * Type of diagnosis associated with a key.
+     */
+    @ReportType
+    public int getReportType() {
+        return reportType;
+    }
+
+    /**
+     * Number of days elapsed between symptom onset and the key being used.
+     *
+     * <p>E.g. 2 means the key is 2 days after onset of symptoms.
+     */
+    public int getDaysSinceOnsetOfSymptoms() {
+        return daysSinceOnsetOfSymptoms;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj instanceof TemporaryExposureKey) {
+            TemporaryExposureKey that = (TemporaryExposureKey) obj;
+            return Arrays.equals(keyData, that.getKeyData())
+                    && Objects.equals(rollingStartIntervalNumber, that.getRollingStartIntervalNumber())
+                    && Objects.equals(transmissionRiskLevel, that.getTransmissionRiskLevel())
+                    && Objects.equals(rollingPeriod, that.getRollingPeriod())
+                    && Objects.equals(daysSinceOnsetOfSymptoms, that.getDaysSinceOnsetOfSymptoms());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(keyData, rollingStartIntervalNumber, transmissionRiskLevel,
+                rollingPeriod, daysSinceOnsetOfSymptoms);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                Locale.US,
+                "TemporaryExposureKey<"
+                        + "keyData: %s, "
+                        + "rollingStartIntervalNumber: %s, "
+                        + "transmissionRiskLevel: %d, "
+                        + "rollingPeriod: %d, "
+                        + "daysSinceOnsetOfSymptoms: %d"
+                        + ">",
+                new BigInteger(1, keyData).toString(16),
+                new Date(MINUTES.toMillis(rollingStartIntervalNumber * 10L)),
+                transmissionRiskLevel,
+                rollingPeriod,
+                daysSinceOnsetOfSymptoms);
+    }
+
+    /**
+     * A builder for {@link TemporaryExposureKey}.
+     */
+    public static final class TemporaryExposureKeyBuilder {
+
+        private byte[] keyData = new byte[0];
+        private int rollingStartIntervalNumber = 0;
+        @RiskLevel
+        private int transmissionRiskLevel = RiskLevel.RISK_LEVEL_INVALID;
+        private int rollingPeriod = 1;
+        @ReportType
+        private int reportType = ReportType.UNKNOWN;
+        private int daysSinceOnsetOfSymptoms = DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN;
+
+        public TemporaryExposureKeyBuilder setKeyData(byte[] keyData) {
+            this.keyData = Arrays.copyOf(keyData, keyData.length);
+            return this;
+        }
+
+        public TemporaryExposureKeyBuilder setRollingStartIntervalNumber(
+                int rollingStartIntervalNumber) {
+            checkArgument(
+                    rollingStartIntervalNumber >= 0,
+                    "rollingStartIntervalNumber (%s) must be >= 0",
+                    rollingStartIntervalNumber);
+            this.rollingStartIntervalNumber = rollingStartIntervalNumber;
+            return this;
+        }
+
+        public TemporaryExposureKeyBuilder setTransmissionRiskLevel(
+                @RiskLevel int transmissionRiskLevel) {
+            checkArgument(
+                    transmissionRiskLevel >= 0 && transmissionRiskLevel <= 8,
+                    "transmissionRiskLevel (%s) must be >= 0 and <= 8",
+                    transmissionRiskLevel);
+            this.transmissionRiskLevel = transmissionRiskLevel;
+            return this;
+        }
+
+        public TemporaryExposureKeyBuilder setRollingPeriod(int rollingPeriod) {
+            checkArgument(rollingPeriod > 0, "rollingPeriod (%s) must be > 0", rollingPeriod);
+            this.rollingPeriod = rollingPeriod;
+            return this;
+        }
+
+        public TemporaryExposureKeyBuilder setReportType(@ReportType int reportType) {
+            checkNotNull(
+                    reportType >= ReportType.UNKNOWN && reportType <= ReportType.REVOKED,
+                    String.format(Locale.getDefault(), "reportType (%d) is invalid", reportType));
+            this.reportType = reportType;
+            return this;
+        }
+
+        public TemporaryExposureKeyBuilder setDaysSinceOnsetOfSymptoms(int daysSinceOnsetOfSymptoms) {
+            checkArgument(
+                    (daysSinceOnsetOfSymptoms >= -14 && daysSinceOnsetOfSymptoms <= 14),
+                    "daysSinceOnsetOfSymptoms (%d) must be >= -14 and <= 14",
+                    daysSinceOnsetOfSymptoms);
+            this.daysSinceOnsetOfSymptoms = daysSinceOnsetOfSymptoms;
+            return this;
+        }
+
+        public TemporaryExposureKey build() {
+            return new TemporaryExposureKey(
+                    keyData,
+                    rollingStartIntervalNumber,
+                    transmissionRiskLevel,
+                    rollingPeriod,
+                    reportType,
+                    daysSinceOnsetOfSymptoms);
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/WakeUpService.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/WakeUpService.java
new file mode 100644
index 0000000..56ad7fa
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/nearby/WakeUpService.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.nearby;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+import androidx.annotation.Nullable;
+
+import com.google.samples.exposurenotification.annotations.Hide;
+import com.google.samples.exposurenotification.threads.TracingHandler;
+import com.google.samples.matching.BuildConfig;
+
+/**
+ * Bindable service which will be used to wake up a valid Exposure Notification client app in the
+ * case where it has been force stopped and is no longer able to provide diagnosis key files in the
+ * background.
+ *
+ * <p>This service does no actual work. It creates a Messenger that can accept messages, but only
+ * then logs any messages that it receives. It is expected for this service to be unbound from
+ * shortly after being bound.
+ *
+ * @hide
+ */
+@Hide
+public class WakeUpService extends Service {
+
+    private static final String TAG = "WakeUpService";
+
+    /** Identifies the name of this service for later binding. */
+    public static final String SERVICE_NAME =
+            "com.google.android.gms.nearby.exposurenotification.WakeUpService";
+
+    /** A simple handler which logs incoming messages. */
+    private static class LoggingHandler extends TracingHandler {
+        @Override
+        public void handleMessage(Message msg) {
+            if (BuildConfig.DEBUG) {
+                Log.i(TAG, "Received message " + msg.what);
+            }
+        }
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (BuildConfig.DEBUG) {
+            Log.i(TAG, "Waking up due to EN binding.");
+        }
+        return new Messenger(new LoggingHandler()).getBinder();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/AbstractSafeParcelable.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/AbstractSafeParcelable.java
new file mode 100644
index 0000000..b992f48
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/AbstractSafeParcelable.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.safeparcel;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Implements {@link SafeParcelable} and implements some default methods
+ * defined by {@link android.os.Parcelable}.
+ *
+ * @hide
+ */
+public abstract class AbstractSafeParcelable implements SafeParcelable {
+
+    public static final Parcelable.Creator<AbstractSafeParcelable> CREATOR = null;
+
+    /** @hide */
+    public final int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        // See the following library for a sample implementation of this:
+        // https://developer.android.com/reference/androidx/versionedparcelable/VersionedParcelable
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/ReflectedParcelable.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/ReflectedParcelable.java
new file mode 100644
index 0000000..cf0eeb5
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/ReflectedParcelable.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.safeparcel;
+
+import android.os.Parcelable;
+
+/**
+ * This interface is used only to preserve compatibility with the annotations used in the Exposure
+ * Notifications code, but the actual implementation is not available here because it's not relevant
+ * to the usage of Exposure Notifications.
+ */
+public interface ReflectedParcelable extends Parcelable {}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/SafeParcelable.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/SafeParcelable.java
new file mode 100644
index 0000000..a856cdc
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/safeparcel/SafeParcelable.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.safeparcel;
+
+import android.os.Parcelable;
+
+/**
+ * This interface is used only to preserve compatibility with the annotations used in the Exposure
+ * Notifications code, but the actual implementation is not available here because it's not relevant
+ * to the usage of Exposure Notifications.
+ */
+public interface SafeParcelable extends Parcelable {
+    /** @hide */
+    public static final String NULL = "SAFE_PARCELABLE_NULL_STRING";
+
+    /** @hide */
+    public @interface Class {
+        String creator();
+        boolean validate() default false;
+    }
+
+    /** @hide */
+    public @interface Field {
+        int id();
+        String getter() default NULL;
+        String type() default NULL;
+        String defaultValue() default NULL;
+        String defaultValueUnchecked() default NULL;
+    }
+
+    /** @hide */
+    public @interface VersionField {
+        int id();
+        String getter() default NULL;
+        String type() default NULL;
+    }
+
+    /** @hide */
+    public @interface Indicator {
+        String getter() default NULL;
+    }
+
+    /** @hide */
+    public @interface Constructor { }
+
+    /** @hide */
+    public @interface Param {
+        int id();
+    }
+
+    /** @hide */
+    public @interface Reserved {
+        int[] value();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/CloseableIterable.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/CloseableIterable.java
new file mode 100644
index 0000000..fc41cc2
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/CloseableIterable.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+import java.io.Closeable;
+
+/**
+ * An {@link Iterable} that can be (sometimes must be) closed to release resource after use.
+ */
+public interface CloseableIterable<T> extends Iterable<T>, Closeable {
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/CloseableIterables.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/CloseableIterables.java
new file mode 100644
index 0000000..145910a
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/CloseableIterables.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+import androidx.annotation.NonNull;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Iterators;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * Provides utility functions for {@link CloseableIterable}.
+ */
+public final class CloseableIterables {
+    /**
+     * A simple implementation of concatenating multiple {@link CloseableIterable}. The returned
+     * iterable will iterate the first {@link CloseableIterable} in {@code in iterables} and then the
+     * second, etc.
+     */
+    public static <T> CloseableIterable<T> concat(List<CloseableIterable<T>> closeableIterables) {
+        if (closeableIterables.isEmpty()) {
+            return emptyInstance();
+        }
+
+        return new CloseableIterable<T>() {
+            @NonNull
+            @Override
+            public Iterator<T> iterator() {
+                return Iterators.concat(
+                        Iterables.transform(closeableIterables, CloseableIterable::iterator).iterator());
+            }
+
+            @Override
+            public void close() throws IOException {
+                for (CloseableIterable<T> closeableIterable : closeableIterables) {
+                    closeableIterable.close();
+                }
+            }
+        };
+    }
+
+    public static <T> CloseableIterable<T> emptyInstance() {
+        return new CloseableIterable<T>() {
+            @Override
+            public void close() throws IOException {
+            }
+
+            @NonNull
+            @Override
+            public Iterator<T> iterator() {
+                return new Iterator<T>() {
+                    @Override
+                    public boolean hasNext() {
+                        return false;
+                    }
+
+                    @Override
+                    public T next() {
+                        throw new NoSuchElementException();
+                    }
+                };
+            }
+        };
+    }
+
+    private CloseableIterables() {
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ContactRecordDataStore.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ContactRecordDataStore.java
new file mode 100644
index 0000000..0fd8081
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ContactRecordDataStore.java
@@ -0,0 +1,464 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.google.common.collect.Iterables;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.ScannedPacket;
+import com.google.samples.exposurenotification.nearby.ScannedPacket.ScannedPacketBuilder;
+import com.google.samples.exposurenotification.nearby.ScannedPacket.ScannedPacketContent;
+import com.google.samples.exposurenotification.nearby.ScannedPacket.ScannedPacketContent.ScannedPacketContentBuilder;
+import com.google.samples.exposurenotification.data.DayNumber;
+import com.google.samples.exposurenotification.data.RollingProximityId;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+
+import org.joda.time.Instant;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+/**
+ * The data store for contact records. Contact records are bundled BLE scan results stored in
+ * key-value pairs. The key is a concatenation of {@link DayNumber} and {@link RollingProximityId}.
+ * The value is a {@link ContactRecordValue};
+ */
+@SuppressWarnings("NewApi")
+public class ContactRecordDataStore implements AutoCloseable {
+
+    private final Map<byte[], byte[]> store = new HashMap<>();
+
+    private ContactRecordDataStore(Context context) throws StorageException {
+    }
+
+    public static synchronized ContactRecordDataStore open(Context context) throws StorageException {
+        return new ContactRecordDataStore(context);
+    }
+
+    @Override
+    public synchronized void close() {
+    }
+
+    /**
+     * Gets the contact records that match the given {@code dayNumber} and {@code rollingProximityId}.
+     *
+     * @return the record if a match is found, or null otherwise.
+     */
+    @Nullable
+    public ContactRecord getRecord(DayNumber dayNumber, RollingProximityId rollingProximityId) {
+        ContactRecordKey key = new ContactRecordKey(dayNumber, rollingProximityId);
+        try {
+            byte[] valueBytes = store.get(key);
+            if (valueBytes == null) {
+                return null;
+            }
+            ContactRecordValue parsedValue = ContactRecordValue.parseFrom(valueBytes);
+            return new ContactRecord(
+                    key,
+                    ContactTracingFeature.contactRecordStoreCompactFormatEnabled()
+                            ? convertToCompactFormat(parsedValue)
+                            : parsedValue);
+        } catch (InvalidProtocolBufferException e) {
+            Log.log.atSevere().withCause(e).log("Error getting record");
+        }
+        return null;
+    }
+
+    public List<ContactRecord> getAllRecords() {
+        List<ContactRecord> records = new ArrayList<>();
+        synchronized (store) {
+            try {
+                for (Entry<byte[], byte[]> iterator : store.entrySet()) {
+                    if (iterator.getKey() == null) {
+                        continue;
+                    }
+                    ContactRecordValue parsedValue =
+                            ContactRecordValue.parseFrom(iterator.getValue());
+                    records.add(
+                            new ContactRecord(
+                                    ContactRecordKey.fromBytes(iterator.getKey()),
+                                    ContactTracingFeature.contactRecordStoreCompactFormatEnabled()
+                                            ? convertToCompactFormat(parsedValue)
+                                            : parsedValue));
+                }
+            } catch (InvalidProtocolBufferException e) {
+                Log.log.atSevere().withCause(e).log("Error fetching record");
+            }
+        }
+        return records;
+    }
+
+    /**
+     * Gets all scan records in {@link ScannedPacket} format, to pass the records from wearable to
+     * phone. Should be used in wearable EN module only.
+     */
+    public List<ScannedPacket> getAllRecordsFromWearable() {
+        ArrayList<ScannedPacket> scannedPacketArrayList = new ArrayList<>();
+        List<ContactRecord> contactRecordList = getAllRecords();
+        Log.log
+                .atInfo()
+                .log(
+                        "getAllRecordsFormWearable called, contactRecordList.size=%d",
+                        contactRecordList.size());
+        for (ContactRecord contactRecord : contactRecordList) {
+            if (contactRecord.getValue().getSightingRecordsCount() <= 0) {
+                continue;
+            }
+            ArrayList<ScannedPacketContent> scannedPacketContents = new ArrayList<>();
+            byte[] metadata = null;
+            for (SightingRecord sightingRecord : contactRecord.value.getSightingRecordsList()) {
+                if (metadata == null) {
+                    metadata = sightingRecord.getAssociatedEncryptedMetadata().toByteArray();
+                }
+                scannedPacketContents.add(
+                        new ScannedPacketContentBuilder()
+                                .setEpochSeconds(sightingRecord.getEpochSeconds())
+                                .setRssi(sightingRecord.getRssi())
+                                .setPreviousScanEpochSeconds(sightingRecord.getPreviousScanEpochSeconds())
+                                .build());
+            }
+            if (metadata == null) {
+                continue;
+            }
+            scannedPacketArrayList.add(
+                    new ScannedPacketBuilder()
+                            .setId(contactRecord.key.rollingProximityId.getDirect())
+                            .setEncryptedMetadata(metadata)
+                            .setScannedPacketContents(scannedPacketContents.toArray(new ScannedPacketContent[0]))
+                            .build());
+        }
+        Log.log
+                .atInfo()
+                .log(
+                        "getAllRecordsFormWearable done, %d scanned packet found",
+                        scannedPacketArrayList.size());
+        return scannedPacketArrayList;
+    }
+
+    /**
+     * Gets all the scanned IDs, for each contact record, only return the 16-byte raw ID.
+     */
+    public List<byte[]> getAllRawIds() {
+        List<byte[]> rawIds = new ArrayList<>();
+        synchronized (store) {
+            for (Entry<byte[], byte[]> iterator : store.entrySet()) {
+                if (iterator.getKey() == null) {
+                    continue;
+                }
+                rawIds.add(ContactRecordKey.getRollingProximityId(iterator.getKey()));
+            }
+        }
+        return rawIds;
+    }
+
+    /**
+     * Adds or updates a contact record value with the key given by {@code dayNumber} and {@code
+     * rollingProximityId}.
+     */
+    @VisibleForTesting
+    public void putRecord(
+            DayNumber dayNumber, RollingProximityId rollingProximityId, ContactRecordValue value) {
+        ContactRecordKey key = new ContactRecordKey(dayNumber, rollingProximityId);
+        synchronized (store) {
+            store.put(key.getBytes(), value.toByteArray());
+        }
+    }
+
+    /**
+     * Appends a {@link SightingRecord} to the {@link ContactRecordValue} keyed by the {@code
+     * dayNumber} and {@code rollingProximityId}.
+     */
+    public void appendSightingRecord(
+            Instant sightTime,
+            byte[] id,
+            int rssi,
+            byte[] associatedEncryptedMetadata,
+            int previousScanEpochSeconds) {
+        DayNumber dayNumber = new DayNumber(sightTime);
+        int sightTimeSeconds = (int) MILLISECONDS.toSeconds(sightTime.getMillis());
+        RollingProximityId rollingProximityId = new RollingProximityId(id);
+
+        if (!ContactTracingFeature.contactRecordStoreCompactFormatEnabled()) {
+            SightingRecord sightingRecord =
+                    SightingRecord.newBuilder()
+                            .setEpochSeconds(sightTimeSeconds)
+                            .setRssi(rssi)
+                            .setAssociatedEncryptedMetadata(ByteString.copyFrom(associatedEncryptedMetadata))
+                            .setPreviousScanEpochSeconds(previousScanEpochSeconds)
+                            .build();
+            ContactRecord contactRecord = getRecord(dayNumber, rollingProximityId);
+            ContactRecordValue contactRecordValue =
+                    contactRecord == null
+                            ? ContactRecordValue.getDefaultInstance()
+                            : contactRecord.getValue();
+            ContactRecordValue updatedContactRecordValue =
+                    contactRecordValue.toBuilder().addSightingRecords(sightingRecord).build();
+            putRecord(dayNumber, rollingProximityId, updatedContactRecordValue);
+            return;
+        }
+
+        ContactRecord contactRecord = getRecord(dayNumber, rollingProximityId);
+        ContactRecordValue existingValue =
+                contactRecord == null ? null : convertToCompactFormat(contactRecord.getValue());
+        ContactRecordValue updatedValue;
+        if (existingValue == null || existingValue.getSightingRecordsCount() == 0) {
+            // No records for this RPI, or the data is corrupted. We create new ContactRecordValue
+            // completely.
+            updatedValue =
+                    ContactRecordValue.newBuilder()
+                            .setEncryptedMetadata(ByteString.copyFrom(associatedEncryptedMetadata))
+                            .addSightingRecords(
+                                    SightingRecord.newBuilder()
+                                            .setEpochSeconds(sightTimeSeconds)
+                                            .setPreviousScanEpochSeconds(previousScanEpochSeconds)
+                                            .setRssiValues(wrapSingleRssi(rssi)))
+                            .build();
+        } else {
+            SightingRecord.Builder sightingRecordBuilder =
+                    Iterables.getLast(existingValue.getSightingRecordsList()).toBuilder();
+            if (isSameScanCycle(sightingRecordBuilder, sightTimeSeconds)) {
+                sightingRecordBuilder.setRssiValues(
+                        sightingRecordBuilder.getRssiValues().concat(wrapSingleRssi(rssi)));
+                backFillEncryptedMetadataIfRequired(
+                        sightingRecordBuilder,
+                        existingValue.getEncryptedMetadata(),
+                        ByteString.copyFrom(associatedEncryptedMetadata));
+                updatedValue =
+                        existingValue.toBuilder()
+                                .setSightingRecords(
+                                        existingValue.getSightingRecordsCount() - 1, sightingRecordBuilder)
+                                .build();
+            } else {
+                updatedValue =
+                        existingValue.toBuilder()
+                                .addSightingRecords(
+                                        SightingRecord.newBuilder()
+                                                .setEpochSeconds(sightTimeSeconds)
+                                                .setPreviousScanEpochSeconds(previousScanEpochSeconds)
+                                                .setRssiValues(wrapSingleRssi(rssi)))
+                                .build();
+            }
+        }
+
+        putRecord(dayNumber, rollingProximityId, updatedValue);
+    }
+
+    private static boolean isSameScanCycle(
+            SightingRecord.Builder sightingRecordBuilder, int sightingTimeSeconds) {
+        // Simple heuristic that considering a new sighting packet to be in the same scan cycle of a
+        // SightingRecord if the elapsed seconds since the beginning of SightingRecord is no bigger than
+        // 1.5x maximum possible scan duration.
+        return sightingTimeSeconds
+                <= sightingRecordBuilder.getEpochSeconds()
+                + (ContactTracingFeature.scanTimeSeconds()
+                + ContactTracingFeature.scanTimeExtendForProfileInUseSeconds())
+                * 1.5;
+    }
+
+    /**
+     * Ensures that `sightingRecordBuilder.encryptedMetadata` has the same (or larger) length as
+     * `sightingRecordBuilder.rssiValues`. If not, fill the slots with the `encryptedMetadata`.
+     */
+    private static void fillMissingEncryptedMetadata(
+            SightingRecord.Builder sightingRecordBuilder, ByteString encryptedMetadata) {
+        if (!sightingRecordBuilder.hasRssiValues()) {
+            return;
+        }
+        while (sightingRecordBuilder.getEncryptedMetadataCount()
+                < sightingRecordBuilder.getRssiValues().size()) {
+            sightingRecordBuilder.addEncryptedMetadata(encryptedMetadata);
+        }
+    }
+
+    @VisibleForTesting
+    static ByteString wrapSingleRssi(int rssi) {
+        return ByteString.copyFrom(new byte[]{(byte) rssi});
+    }
+
+    /**
+     * Back fills {@link SightingRecord.Builder#getEncryptedMetadataList()} per format requirement: If
+     * {@code currentEncryptedMetadata} is not equal to {@code baseEncryptedMetadata}, we must fill
+     * encrypted metadata for this sighting packet as well as all previous packets if absent. If not
+     * equal, we do not need to fill it as long as the whole {@link
+     * SightingRecord.Builder#getEncryptedMetadataList()} is empty, otherwise we still need to set it.
+     *
+     * @param sightingRecordBuilder    the builder to update
+     * @param baseEncryptedMetadata    the encrypted metadata in {@link ContactRecordValue}.
+     * @param currentEncryptedMetadata the encrypted metadata associated with the last element of
+     *                                 {@link SightingRecord.Builder#getRssiValues()}.
+     */
+    private static void backFillEncryptedMetadataIfRequired(
+            SightingRecord.Builder sightingRecordBuilder,
+            ByteString baseEncryptedMetadata,
+            ByteString currentEncryptedMetadata) {
+        if (sightingRecordBuilder.getEncryptedMetadataCount() != 0
+                || !currentEncryptedMetadata.equals(baseEncryptedMetadata)) {
+            fillMissingEncryptedMetadata(sightingRecordBuilder, baseEncryptedMetadata);
+            sightingRecordBuilder.setEncryptedMetadata(
+                    sightingRecordBuilder.getEncryptedMetadataCount() - 1, currentEncryptedMetadata);
+        }
+    }
+
+    /**
+     * Coverts a {@link ContactRecordValue} to the "compact format" if it is not already in that
+     * format.
+     *
+     * <p>We can remove the conversion code once the build using compact format has been rolled out
+     * for a while.
+     */
+    private static ContactRecordValue convertToCompactFormat(ContactRecordValue originalValue) {
+        if (originalValue.hasEncryptedMetadata()) {
+            return originalValue;
+        }
+
+        ContactRecordValue.Builder updatedValueBuilder = ContactRecordValue.newBuilder();
+        SightingRecord.Builder sightingRecordBuilder = null;
+        for (SightingRecord sightingRecord : originalValue.getSightingRecordsList()) {
+            // Set the base encrypted metadata to be the first one we see from the list.
+            if (!updatedValueBuilder.hasEncryptedMetadata()) {
+                updatedValueBuilder.setEncryptedMetadata(sightingRecord.getAssociatedEncryptedMetadata());
+            }
+
+            if (sightingRecordBuilder != null
+                    && isSameScanCycle(sightingRecordBuilder, sightingRecord.getEpochSeconds())) {
+                sightingRecordBuilder.setRssiValues(
+                        sightingRecordBuilder.getRssiValues().concat(wrapSingleRssi(sightingRecord.getRssi())));
+            } else {
+                if (sightingRecordBuilder != null) {
+                    updatedValueBuilder.addSightingRecords(sightingRecordBuilder);
+                }
+                sightingRecordBuilder =
+                        sightingRecord.toBuilder()
+                                .setRssiValues(wrapSingleRssi(sightingRecord.getRssi()))
+                                .clearAssociatedEncryptedMetadata()
+                                .clearRssi();
+            }
+            backFillEncryptedMetadataIfRequired(
+                    sightingRecordBuilder,
+                    updatedValueBuilder.getEncryptedMetadata(),
+                    sightingRecord.getAssociatedEncryptedMetadata());
+        }
+        if (sightingRecordBuilder != null) {
+            updatedValueBuilder.addSightingRecords(sightingRecordBuilder);
+        }
+
+        return updatedValueBuilder.build();
+    }
+
+    /**
+     * Deletes all contact records on a day given by the {@link DayNumber}.
+     *
+     * @return the total number of entries deleted
+     */
+    public int deletesRecords(DayNumber dayNumber) throws StorageException {
+        // FIXME: delete records for a given day number.
+        return 0;
+    }
+
+    /**
+     * Deletes all contact records prior (inclusive) to a day given by the {@link DayNumber}.
+     *
+     * @return the total number of entries deleted
+     */
+    public int deletePriorRecords(DayNumber lastDayNumberToDelete) throws StorageException {
+        // FIXME: Delete prescribed records.
+        return 0;
+    }
+
+    /**
+     * An immutable data class represents a contact record. The key-value pair data can be accessed
+     * through {@link #getKey()} and {@link #getValue()}.
+     */
+    public static class ContactRecord {
+        private final ContactRecordKey key;
+        private final ContactRecordValue value;
+
+        public ContactRecord(ContactRecordKey key, ContactRecordValue value) {
+            this.key = key;
+            this.value = value;
+        }
+
+        public ContactRecordKey getKey() {
+            return key;
+        }
+
+        public ContactRecordValue getValue() {
+            return value;
+        }
+    }
+
+    /**
+     * An immutable data class represents the key for a {@link ContactRecord}. The byte array
+     * representation of this class is used as the key of underlying data store.
+     */
+    public static class ContactRecordKey {
+        private final DayNumber dayNumber;
+        private final RollingProximityId rollingProximityId;
+
+        public ContactRecordKey(DayNumber dayNumber, RollingProximityId rollingProximityId) {
+            this.dayNumber = dayNumber;
+            this.rollingProximityId = rollingProximityId;
+        }
+
+        public DayNumber getDayNumber() {
+            return dayNumber;
+        }
+
+        public RollingProximityId getRollingProximityId() {
+            return rollingProximityId;
+        }
+
+        private static byte[] getRollingProximityId(byte[] contactRecordKeyBytes) {
+            return Arrays.copyOfRange(
+                    contactRecordKeyBytes, DayNumber.getSizeBytes(), contactRecordKeyBytes.length);
+        }
+
+        /**
+         * Gets the byte array representation of the key.
+         */
+        public byte[] getBytes() {
+            ByteBuffer byteBuffer =
+                    ByteBuffer.allocate(DayNumber.getSizeBytes() + rollingProximityId.length);
+
+            dayNumber.putIn(byteBuffer);
+            rollingProximityId.putIn(byteBuffer);
+
+            return byteBuffer.array();
+        }
+
+        public static ContactRecordKey fromBytes(byte[] bytes) {
+            ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+            DayNumber dayNumber = DayNumber.getFrom(byteBuffer);
+            byte[] rollingProximityIdBytes = new byte[RollingProximityId.MIN_ID.length];
+            byteBuffer.get(rollingProximityIdBytes);
+            return new ContactRecordKey(dayNumber, new RollingProximityId(rollingProximityIdBytes, true));
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/Encoder.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/Encoder.java
new file mode 100644
index 0000000..1ffc759
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/Encoder.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+import com.google.common.collect.ImmutableList;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import static com.google.common.hash.Hashing.sha256;
+
+/**
+ * An encoder that encodes some input into a byte array.
+ *
+ * <p>To use, extends this class and create a constructor that accepts an input of any type.
+ * Implement {@link #encodeToBuffer(ByteBuffer)} and use the class by instantiating and calling
+ * {@link #encode()}. To chain a series of these, see {@link SerialEncoder}.
+ */
+public abstract class Encoder {
+
+    /**
+     * "Encode" or "decode" a byte array for saving or restoring from leveldb. Mainly a dummy wrapper
+     * class for easy chaining. See {@link SerialEncoder}.
+     */
+    public static class BytesEncoder extends Encoder {
+
+        private final byte[] bytes;
+
+        public BytesEncoder(byte[] bytes) {
+            this.bytes = bytes;
+        }
+
+        @Override
+        protected void encodeToBuffer(ByteBuffer byteBuffer) {
+            byteBuffer.put(bytes);
+        }
+
+        @Override
+        public int numBytes() {
+            return bytes.length;
+        }
+    }
+
+    /**
+     * An encoder that takes a list of encoders to encode a series of value.
+     */
+    public static abstract class SerialEncoder extends Encoder {
+
+        private final List<Encoder> encoders;
+
+        protected SerialEncoder(ImmutableList<Encoder> encoders) {
+            this.encoders = encoders;
+        }
+
+        @Override
+        protected void encodeToBuffer(ByteBuffer byteBuffer) {
+            for (Encoder encoder : encoders) {
+                encoder.encodeToBuffer(byteBuffer);
+            }
+        }
+
+        @Override
+        protected int numBytes() {
+            int numBytes = 0;
+            for (Encoder encoder : encoders) {
+                numBytes += encoder.numBytes();
+            }
+            return numBytes;
+        }
+    }
+
+    /**
+     * Encodes a {@link String} into a byte array by Sha256ing the {@link
+     * java.nio.charset.StandardCharsets#UTF_8} encoding of the input string.
+     */
+    @SuppressWarnings("UnstableApiUsage")
+    public static class Sha256StringEncoder extends Encoder {
+
+        private final String string;
+
+        // TODO(wukevin,enxun): Revisit semantics around Encoder classes.
+        public Sha256StringEncoder(String string) {
+            this.string = string;
+        }
+
+        @Override
+        protected void encodeToBuffer(ByteBuffer byteBuffer) {
+            byteBuffer.put(sha256().hashUnencodedChars(string).asBytes());
+        }
+
+        @Override
+        protected int numBytes() {
+            return sha256().bits() / 8;
+        }
+    }
+
+    /**
+     * Encodes value with {@link #numBytes()} into {@link ByteBuffer}.
+     */
+    protected abstract void encodeToBuffer(ByteBuffer byteBuffer);
+
+    /**
+     * The number of bytes that will be written when {@link #encodeToBuffer(ByteBuffer)} is called.
+     */
+    protected abstract int numBytes();
+
+    /**
+     * Returns a byte array containing the encoded version {@link Encoder} wrapped values.
+     */
+    public byte[] encode() {
+        ByteBuffer byteBuffer = ByteBuffer.allocate(numBytes());
+        encodeToBuffer(byteBuffer);
+        return byteBuffer.array();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/EncodingComparisonHelper.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/EncodingComparisonHelper.java
new file mode 100644
index 0000000..1e45b85
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/EncodingComparisonHelper.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+/** Encoding helper function for comparing the key. */
+public class EncodingComparisonHelper {
+
+    /**
+     * Returns true if the {@code root} is a sub array of {@code key}.
+     *
+     * <p>A null input for root will always return true.
+     */
+    public static boolean hasRoot(byte[] root, byte[] key) {
+        if (root.length > key.length) {
+            return false;
+        }
+        for (int i = 0; i < root.length && i < key.length; ++i) {
+            if (root[i] != key[i]) {
+                return false;
+            }
+        }
+        return root.length <= key.length;
+    }
+
+    private EncodingComparisonHelper() {}
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ExposureConfigurationConverter.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ExposureConfigurationConverter.java
new file mode 100644
index 0000000..83e9aa8
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ExposureConfigurationConverter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+import com.google.common.base.Converter;
+import com.google.common.primitives.Ints;
+import com.google.samples.exposurenotification.nearby.ExposureConfiguration;
+import com.google.samples.exposurenotification.nearby.ExposureConfiguration.ExposureConfigurationBuilder;
+
+/**
+ * Converts between the Android Parcelable {@link ExposureConfiguration} and the proto {@link
+ * ExposureConfigurationProto}.
+ *
+ * <p>Why this converter? https://developer.android.com/reference/android/os/Parcel.html.
+ */
+public class ExposureConfigurationConverter
+        extends Converter<ExposureConfiguration, ExposureConfigurationProto> {
+
+    @Override
+    protected ExposureConfigurationProto doForward(ExposureConfiguration config) {
+        return ExposureConfigurationProto.newBuilder()
+                .setMinimumRiskScore(config.getMinimumRiskScore())
+                .setAttenuationWeight(config.getAttenuationWeight())
+                .addAllAttenuationScores(Ints.asList(config.getAttenuationScores()))
+                .setDaysSinceLastExposureWeight(config.getDaysSinceLastExposureWeight())
+                .addAllDaysSinceLastExposureScores(Ints.asList(config.getDaysSinceLastExposureScores()))
+                .setDurationWeight(config.getDurationWeight())
+                .addAllDurationScores(Ints.asList(config.getDurationScores()))
+                .setTransmissionRiskWeight(config.getTransmissionRiskWeight())
+                .addAllTransmissionRiskScores(Ints.asList(config.getTransmissionRiskScores()))
+                .addAllDurationAtAttenuationThresholds(
+                        Ints.asList(config.getDurationAtAttenuationThresholds()))
+                .build();
+    }
+
+    @Override
+    protected ExposureConfiguration doBackward(ExposureConfigurationProto proto) {
+        return new ExposureConfigurationBuilder()
+                .setMinimumRiskScore(proto.getMinimumRiskScore())
+                .setAttenuationWeight(proto.getAttenuationWeight())
+                .setAttenuationScores(Ints.toArray(proto.getAttenuationScoresList()))
+                .setDaysSinceLastExposureWeight(proto.getDaysSinceLastExposureWeight())
+                .setDaysSinceLastExposureScores(Ints.toArray(proto.getDaysSinceLastExposureScoresList()))
+                .setDurationWeight(proto.getDurationWeight())
+                .setDurationScores(Ints.toArray(proto.getDurationScoresList()))
+                .setTransmissionRiskWeight(proto.getTransmissionRiskWeight())
+                .setTransmissionRiskScores(Ints.toArray(proto.getTransmissionRiskScoresList()))
+                .setDurationAtAttenuationThresholds(
+                        Ints.toArray(proto.getDurationAtAttenuationThresholdsList()))
+                .build();
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ExposureResultStorage.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ExposureResultStorage.java
new file mode 100644
index 0000000..68ed32e
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/ExposureResultStorage.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+import android.content.Context;
+
+import androidx.annotation.Nullable;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.data.DayNumber;
+import com.google.samples.exposurenotification.features.ContactTracingFeature;
+import com.google.samples.exposurenotification.storage.Encoder.SerialEncoder;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Storage database that stores {@link ExposureResult}s.
+ *
+ * <p>The store is keyed by PackageNameHash:Signature:TokenHash:TemporaryExposureKey. See {@link
+ * KeyEncoder} for the details of the encoding.
+ */
+public class ExposureResultStorage implements AutoCloseable {
+
+    private final List<Row> transactionRows = new ArrayList<>();
+    private final Map<byte[], byte[]> threadSafeLevelDb = new HashMap<>();
+
+    public static ExposureResultStorage open(Context context) throws StorageException {
+        return new ExposureResultStorage(context);
+    }
+
+    private ExposureResultStorage(Context context) throws StorageException {
+    }
+
+    /**
+     * Checks whether there's a {@link ExposureResult} calculated for the specified input already.
+     *
+     * <p>See {@link Row} for parameter definitions.
+     *
+     * @deprecated use hasResult(byte[], byte[]) instead.
+     */
+    @Deprecated
+    public boolean hasResult(
+            String packageName,
+            byte[] signatureHash,
+            String token,
+            TemporaryExposureKey temporaryExposureKey) {
+        Preconditions.checkArgument(signatureHash.length == 32, "Signature hash not Sha256 length.");
+        return threadSafeLevelDb.get(
+                new KeyEncoder(packageName, signatureHash, token, temporaryExposureKey.getKeyData())
+                        .encode())
+                != null;
+    }
+
+    /**
+     * Checks whether there's a {@link ExposureResult} calculated for the specified input already.
+     *
+     * <p>See {@link Row} for parameter definitions.
+     */
+    public boolean hasResult(byte[] tokenRoot, byte[] exposureKey) {
+        for (Row row : transactionRows) {
+            if (row.tokenRoot() != null
+                    && Arrays.equals(row.tokenRoot(), tokenRoot)
+                    && Arrays.equals(row.key().getKeyData(), exposureKey)) {
+                return true;
+            }
+        }
+        return threadSafeLevelDb.get(new KeyEncoder(tokenRoot, exposureKey).encode()) != null;
+    }
+
+    /**
+     * Stores a single result. Returns true if successfully stored, false otherwise.
+     *
+     * @deprecated use {@link #storeResult(byte[], TemporaryExposureKey, ExposureResult, boolean)} instead.
+     */
+    @Deprecated
+    public boolean storeResult(Row row) {
+        threadSafeLevelDb.put(new KeyEncoder(row).encode(), row.exposureResult().toByteArray());
+        return true;
+    }
+
+    /**
+     * Stores a single result. Returns true if successfully stored, false otherwise.
+     */
+    public boolean storeResult(
+            byte[] tokenRoot,
+            TemporaryExposureKey exposureKey,
+            ExposureResult exposureResult,
+            boolean holdForTransaction) {
+        if (holdForTransaction) {
+            transactionRows.add(
+                    Row.builder()
+                            .setTokenRoot(tokenRoot)
+                            .setKey(exposureKey)
+                            .setExposureResult(exposureResult)
+                            .build());
+            return true;
+        } else {
+            threadSafeLevelDb.put(
+                    new KeyEncoder(tokenRoot, exposureKey.getKeyData()).encode(),
+                    exposureResult.toByteArray());
+            return true;
+        }
+    }
+
+    /**
+     * Commits {@link #storeResult} requests when processed using holdForTransaction, completing the
+     * write.
+     */
+    public void commitStoreResultRequestsForTransaction() {
+        storeResults(transactionRows);
+        transactionRows.clear();
+    }
+
+    /**
+     * Stores list of results. Returns true if succeeds, false otherwise.
+     */
+    public boolean storeResults(List<Row> results) {
+        synchronized (threadSafeLevelDb) {
+            for (Row row : results) {
+                byte[] tokenRoot = row.tokenRoot();
+                KeyEncoder keyEncoder =
+                        tokenRoot != null
+                                ? new KeyEncoder(tokenRoot, row.key().getKeyData())
+                                : new KeyEncoder(row);
+                threadSafeLevelDb.put(keyEncoder.encode(), row.exposureResult().toByteArray());
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Returns an {@link ExposureResult} calculated for the specified input already.
+     *
+     * <p>See {@link Row} for parameter definitions.
+     */
+    public ExposureResult getResult(byte[] tokenRoot, byte[] exposureKey) {
+        try {
+            byte[] result = threadSafeLevelDb.get(new KeyEncoder(tokenRoot, exposureKey).encode());
+            if (result != null) {
+                return ExposureResult.parseFrom(result);
+            }
+        } catch (InvalidProtocolBufferException e) {
+            Log.log.atSevere().withCause(e).log("Error checking for result.");
+        }
+        return ExposureResult.getDefaultInstance();
+    }
+
+    /**
+     * Gets the exposure results for specified package and token.
+     */
+    public List<ExposureResult> getAll(String packageName, byte[] signatureHash, String token)
+            throws StorageException {
+        Preconditions.checkArgument(signatureHash.length == 32, "Signature hash not Sha256 length.");
+        byte[] requestKeyRoot = new TokenRootEncoder(packageName, signatureHash, token).encode();
+
+        ImmutableList.Builder<ExposureResult> exposureResultsBuilder = ImmutableList.builder();
+        synchronized (threadSafeLevelDb) {
+            for (Entry<byte[], byte[]> iterator : threadSafeLevelDb.entrySet()) {
+                if (EncodingComparisonHelper.hasRoot(requestKeyRoot, iterator.getKey())) {
+                    try {
+                        exposureResultsBuilder.add(ExposureResult.parseFrom(iterator.getValue()));
+                    } catch (InvalidProtocolBufferException e) {
+                        Log.log.atSevere().withCause(e).log("Unable to parse exposure result for key.");
+                    }
+                }
+
+            }
+        }
+        return exposureResultsBuilder.build();
+    }
+
+    /**
+     * Delete results associated with provided package. Returns number of items deleted.
+     */
+    public int deleteAll(String packageName, byte[] signatureHash) {
+        Preconditions.checkState(signatureHash.length == 32, "Signature hash not Sha256 length.");
+        byte[] packageKeyRoot = new PackageRootEncoder(packageName, signatureHash).encode();
+        int deleted = 0;
+        synchronized (threadSafeLevelDb) {
+            Iterator<Entry<byte[], byte[]>> iterator = threadSafeLevelDb.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Entry<byte[], byte[]> item = iterator.next();
+                if (EncodingComparisonHelper.hasRoot(packageKeyRoot, item.getKey())) {
+                    threadSafeLevelDb.remove(item.getKey());
+                }
+            }
+        }
+        return deleted;
+    }
+
+    /**
+     * Deletes all {@link ExposureResult}s in store.
+     */
+    public int deleteAll() {
+        final int keysPurged;
+        synchronized (threadSafeLevelDb) {
+            keysPurged = threadSafeLevelDb.size();
+            threadSafeLevelDb.clear();
+        }
+        return keysPurged;
+    }
+
+    /**
+     * Delete results up to and including {@code lastDayToDelete}.
+     *
+     * <p>Returns number of results purged.
+     */
+    public int deletePrior(DayNumber lastDayToDelete) {
+        // FIXME: Delete prescribed results.
+        return 0;
+    }
+
+    @Override
+    public void close() {
+        /* nothing to close */
+    }
+
+    /**
+     * A row in {@link ExposureResultStorage}.
+     */
+    @AutoValue
+    public abstract static class Row {
+
+        /**
+         * Builder for {@link Row}.
+         */
+        @AutoValue.Builder
+        public abstract static class Builder {
+
+            public Row build() {
+                Row row = autoBuild();
+                byte[] signatureHash = row.signatureHash();
+                if (signatureHash != null) {
+                    Preconditions.checkState(signatureHash.length == 32, "Signature hash not Sha256 length.");
+                }
+                return row;
+            }
+
+            abstract Row autoBuild();
+
+            public abstract Builder setKey(TemporaryExposureKey temporaryTracingKey);
+
+            @Deprecated
+            public abstract Builder setPackageName(String packageName);
+
+            @Deprecated
+            public abstract Builder setSignatureHash(byte[] signatureHash);
+
+            @Deprecated
+            public abstract Builder setToken(String token);
+
+            public abstract Builder setTokenRoot(byte[] tokenRoot);
+
+            public abstract Builder setExposureResult(ExposureResult exposureResult);
+        }
+
+        public static Builder builder() {
+            return new AutoValue_ExposureResultStorage_Row.Builder();
+        }
+
+        /**
+         * {@link TemporaryExposureKey} this {@link ExposureResult} was calculated for.
+         */
+        public abstract TemporaryExposureKey key();
+
+        /**
+         * Package that requested this row's {@link ExposureResult}.
+         */
+        @Deprecated
+        @Nullable
+        public abstract String packageName();
+
+        /**
+         * Package signature required to differentiate client apps.
+         */
+        @Deprecated
+        @Nullable
+        public abstract byte[] signatureHash();
+
+        /**
+         * Token this {@link ExposureResult} was provided with.
+         */
+        @Deprecated
+        @Nullable
+        abstract String token();
+
+        @Nullable
+        abstract byte[] tokenRoot();
+
+        /**
+         * {@link ExposureResult} calculated for.
+         */
+        abstract ExposureResult exposureResult();
+    }
+
+    /**
+     * See {@link KeyEncoder} for details.
+     */
+    private static class PackageRootEncoder extends SerialEncoder {
+
+        PackageRootEncoder(String packageName, byte[] signatureHash) {
+            super(
+                    ImmutableList.of(new Sha256StringEncoder(packageName), new BytesEncoder(signatureHash)));
+        }
+    }
+
+    /**
+     * See {@link KeyEncoder} for details.
+     */
+    private static class TokenRootEncoder extends SerialEncoder {
+
+        TokenRootEncoder(String packageName, byte[] signatureHash, String token) {
+            super(
+                    ImmutableList.of(
+                            new PackageRootEncoder(packageName, signatureHash), new Sha256StringEncoder(token)));
+        }
+    }
+
+    /**
+     * Encodes token root into byte array.
+     */
+    public static byte[] encodeTokenRoot(String packageName, byte[] signatureHash, String token) {
+        return new ExposureResultStorage.TokenRootEncoder(packageName, signatureHash, token).encode();
+    }
+
+    /**
+     * This {@link Encoder} is a four part encoding:
+     *
+     * <ol>
+     *   <li>Sha256 hashed package name
+     *   <li>Signature byte array (assumed to be Sha256)
+     *   <li>Sha256 hashed token
+     *   <li>Exposure key byte array ({@link ContactTracingFeature#contactIdLength()} in length)
+     * </ol>
+     * <p>
+     * To encode only up to each of the parts, use the respective root encoders above:
+     *
+     * <ul>
+     *   <li>To encode up to package and signature, use {@link PackageRootEncoder}
+     *   <li>To encode up to token, use {@link TokenRootEncoder}
+     * </ul>
+     */
+    private static class KeyEncoder extends SerialEncoder {
+
+        KeyEncoder(byte[] tokenRoot, byte[] exposureKey) {
+            super(ImmutableList.of(new BytesEncoder(tokenRoot), new BytesEncoder(exposureKey)));
+        }
+
+        /**
+         * @deprecated use {@link #KeyEncoder(byte[], byte[])} instead.
+         */
+        @Deprecated
+        KeyEncoder(String packageName, byte[] signatureHash, String token, byte[] exposureKey) {
+            super(
+                    ImmutableList.of(
+                            new TokenRootEncoder(packageName, signatureHash, token),
+                            new BytesEncoder(exposureKey)));
+        }
+
+        /**
+         * @deprecated use {@link #KeyEncoder(byte[], byte[])} instead.
+         */
+        @Deprecated
+        @SuppressWarnings("nullness")
+        KeyEncoder(Row row) {
+            this(row.packageName(), row.signatureHash(), row.token(), row.key().getKeyData());
+        }
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/SelfTemporaryExposureKeyDataStore.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/SelfTemporaryExposureKeyDataStore.java
new file mode 100644
index 0000000..4d7abde
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/SelfTemporaryExposureKeyDataStore.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+import androidx.annotation.NonNull;
+
+import com.google.samples.exposurenotification.Log;
+import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
+import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * TODO: Implement a way to store {@link TemporaryExposureKey}s.
+ */
+public class SelfTemporaryExposureKeyDataStore {
+    private List<TemporaryExposureKey> storedKeys = new ArrayList<>();
+
+    public void putKey(TemporaryExposureKey temporaryExposureKey) {
+        if (!storedKeys.contains(temporaryExposureKey)) {
+            storedKeys.add(temporaryExposureKey);
+        }
+    }
+
+    public boolean hasKey(TemporaryExposureKey temporaryExposureKey) {
+        return storedKeys.contains(temporaryExposureKey);
+    }
+
+    public CloseableIterable<TemporaryExposureKey> getAllKeys() throws StorageException {
+        return new CloseableIterable<TemporaryExposureKey>() {
+            @Override
+            public void close() throws IOException {
+                // Nothing to close in this stub.
+            }
+
+            @NonNull
+            @Override
+            public Iterator<TemporaryExposureKey> iterator() {
+                return storedKeys.iterator();
+            }
+        };
+    }
+
+    public TemporaryExposureKey getSingleKeyForIntervalNumber(int intervalNumber) {
+        Log.log.atInfo().log("Getting exposure key that covers interval number=%d.", intervalNumber);
+        try (CloseableIterable<TemporaryExposureKey> iterableStoredKeys = getAllKeys()) {
+            for (TemporaryExposureKey key : iterableStoredKeys) {
+                if (key.getRollingStartIntervalNumber() <= intervalNumber
+                        && intervalNumber < TemporaryExposureKeySupport.getRollingEndIntervalNumber(key)) {
+                    Log.log.atInfo().log("Exposure key found");
+                    return key;
+                }
+            }
+        } catch (StorageException | IOException e) {
+            Log.log.atWarning().withCause(e).log("Exposure key not found.");
+            return null;
+        }
+        Log.log.atInfo().log("Exposure key not found.");
+        return null;
+    }
+}
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/StorageException.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/StorageException.java
new file mode 100644
index 0000000..2c538fa
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/storage/StorageException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.samples.exposurenotification.storage;
+
+/** A storage exception in exposure notification. */
+public class StorageException extends Exception {
+    public StorageException(String msg) {
+        super(msg);
+    }
+
+    public StorageException(Throwable cause) {
+        super(cause);
+    }
+
+    public StorageException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/java/com/google/samples/exposurenotification/threads/TracingHandler.java b/exposurenotification/src/main/java/com/google/samples/exposurenotification/threads/TracingHandler.java
new file mode 100644
index 0000000..dc2c826
--- /dev/null
+++ b/exposurenotification/src/main/java/com/google/samples/exposurenotification/threads/TracingHandler.java
@@ -0,0 +1,11 @@
+package com.google.samples.exposurenotification.threads;
+
+import android.os.Handler;
+
+/**
+ * This interface is used only to preserve compatibility with the annotations used in the Exposure
+ * Notifications code, but the actual implementation is not available here because it's not relevant
+ * to the usage of Exposure Notifications.
+ */
+public class TracingHandler extends Handler {
+}
diff --git a/exposurenotification/src/main/proto/api_call_record.proto b/exposurenotification/src/main/proto/api_call_record.proto
new file mode 100644
index 0000000..a388228
--- /dev/null
+++ b/exposurenotification/src/main/proto/api_call_record.proto
@@ -0,0 +1,133 @@
+syntax = "proto2";
+
+package com.google.samples.exposurenotification.storage;
+
+import "exposure_key_export.proto";
+import "exposure_result.proto";
+import "matching_request.proto";
+
+option java_multiple_files = true;
+option java_package = "com.google.samples.exposurenotification.storage";
+
+// the collection of all advertisement records.
+// next index: 2
+message ApiCallRecords {
+  repeated ApiCallRecord records = 1;
+}
+
+// A record for one API call.
+// next index: 13
+message ApiCallRecord {
+  // The package name of the client app.
+  optional string package_name = 1;
+  // Epoch timestamp in milliseconds of the API call.
+  optional int64 timestamp_millis = 2;
+  // The parameters of the API call. Refer to API definition.
+  oneof method_call {
+    StartMethod start = 3;
+    StopMethod stop = 4;
+    IsEnabledMethod is_enabled = 5;
+    GetTemporaryExposureKeyHistoryMethod get_temporary_exposure_key_history = 6;
+    ProvideDiagnosisKeysMethod provide_diagnosis_keys = 7;
+    GetExposureSummaryMethod get_exposure_summary = 8;
+    GetExposureInformationMethod get_exposure_information = 9;
+    GetExposureWindowsMethod get_exposure_windows = 12;
+  }
+  // Status code of the API call.
+  optional int32 status_code = 10;
+  // Signature Hash of the calling package.
+  optional bytes signature_hash = 11;
+}
+
+// start() method.
+message StartMethod {
+  // Human-readable application name.
+  optional string app_name = 1;
+}
+
+// stop() method.
+message StopMethod {}
+
+// isEnabled() method
+message IsEnabledMethod {
+  // Return value of this method.
+  optional bool return_value = 1;
+}
+
+// getTemporaryExposureKeyHistory() method.
+message GetTemporaryExposureKeyHistoryMethod {
+  // Return value of this method.
+  repeated TemporaryExposureKey return_value = 1;
+}
+
+// Represents the information about a client provided diagnosis key file.
+message DiagnosisKeyFile {
+  // The size of the zip archive, in bytes.
+  optional uint32 zip_file_size = 1;
+  // Whether the zip contains signature.
+  optional bool has_signature = 2;
+  // Size of the key file after decompression, in bytes.
+  optional uint32 key_file_size = 3;
+}
+
+// Some information of a list of key files provided by the client side.
+message DiagnosisKeyFiles {
+  repeated DiagnosisKeyFile key_files = 1;
+  // A hash of the files in the current batch.
+  optional bytes file_hash = 2;
+}
+
+// provideDiagnosisKeys() method.
+message ProvideDiagnosisKeysMethod {
+  // The paths of all the key files.
+  optional DiagnosisKeyFiles key_files = 1;
+  // Exposure configuration.
+  optional ExposureConfigurationProto exposure_configuration = 2;
+  // Unique token for this batch.
+  optional string token = 3;
+}
+
+// Proto equivalence for
+// com.google.samples.exposurenotification.ExposureSummary.
+message ExposureSummaryProto {
+  optional int32 days_since_last_exposure = 1;
+  optional int32 matched_key_count = 2;
+  optional int32 maximum_risk_score = 3;
+  repeated int32 attenuation_durations = 4;
+  optional int32 summation_risk_score = 5;
+}
+
+// getExposureSummary() method.
+message GetExposureSummaryMethod {
+  // Return value of this method.
+  optional ExposureSummaryProto return_value = 1;
+  // Unique token for this batch.
+  optional string token = 2;
+}
+
+// Proto equivalence for
+// com.google.samples.exposurenotification.ExposureInformation.
+message ExposureInformationProto {
+  optional int64 date_millis_since_epoch = 1;
+  optional int32 duration_minutes = 2;
+  optional int32 attenuation_value = 3;
+  optional int32 transmission_risk_level = 4;
+  optional int32 total_risk_score = 5;
+  repeated int32 attenuation_durations = 6;
+}
+
+// getExposureInformation() method.
+message GetExposureInformationMethod {
+  // Return value of this method.
+  repeated ExposureInformationProto return_value = 1;
+  // Unique token for this batch.
+  optional string token = 2;
+}
+
+// getExposureWindows() method.
+message GetExposureWindowsMethod {
+  // Return value of this method.
+  repeated ExposureWindowProto return_value = 1;
+  // Unique token for this batch.
+  optional string token = 2;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/proto/client_record.proto b/exposurenotification/src/main/proto/client_record.proto
new file mode 100644
index 0000000..4ef70d4
--- /dev/null
+++ b/exposurenotification/src/main/proto/client_record.proto
@@ -0,0 +1,147 @@
+syntax = "proto2";
+
+package com.google.samples.exposurenotification.storage;
+
+import "exposure_key_export.proto";
+import "exposure_notification_enums.proto";
+
+option java_multiple_files = true;
+option java_package = "com.google.samples.exposurenotification.storage";
+
+// the collection of all client records
+// next index: 2
+message ClientRecords {
+  repeated ClientRecord client_records = 1;
+}
+
+// a record of all information for a single client
+// next index: 11
+message ClientRecord {
+  // the client's android package name
+  optional string package_name = 1;
+
+  // sha256 hash of the client's package signature
+  optional bytes signature_hash = 2;
+
+  // whether the user has consented to tracking for exposure notifications
+  // (which does not imply the user has consented to sharing their own
+  // diagnosis), and is also not the same as whether exposure notification is
+  // currently active for this client
+  optional bool exposure_notification_consent = 3;
+
+  // if set, the client has requested active exposure notifications
+  optional ExposureNotificationRequest exposure_notification_request = 4;
+
+  // the last time "Share Keys" dialog was accepted by user;
+  // since v1.6+, controlled by flag, we require dialog every time (i.e.
+  // disabled grace period), so is then treated as a boolean:
+  // hasShareKeysDialogConsentedMillis: true - allow, false - show dialog.
+  optional int64 share_keys_dialog_consented_millis = 5;
+
+  // Last known timestamp for which matching was last performed for client
+  optional int64 last_matching_timestamp_millis = 6;
+
+  // client specific throttling state (for various APIs)
+  optional ClientThrottling client_throttling = 7;
+
+  // An archived list of completed matching request records for this client
+  repeated CompletedMatchingRequestRecord completed_matching_request_records =
+      8;
+  // Last known timestamp for which the client requested active exposure
+  // notifications
+  optional int64 last_exposure_notification_request_set_timestamp_millis = 9;
+
+  // configuration of TEKs for calculating risk score.
+  optional TekMetadataRecord tek_metadata_record = 10;
+}
+
+// all data associated with a client's exposure notification request
+// next index: 3
+message ExposureNotificationRequest {
+  // exposure configuration options
+  // deprecated because exposure configuration is no longer in start() API.
+  optional ExposureConfigurationInfo exposure_configuration = 1
+  [deprecated = true];
+
+  // when the request has been set for the client; means it's been active since
+  // then
+  optional int64 created_millis = 2;
+}
+
+// configuration options on how to determine if a client has been exposed. see
+// documentation for ExposureConfiguration client class for more information.
+// next index: 10
+message ExposureConfigurationInfo {
+  optional int32 minimum_risk_score = 1;
+
+  repeated int32 bucketed_attenuation_scores = 2;
+
+  optional int32 attenuation_weight = 3;
+
+  repeated int32 bucketed_latency_days_scores = 4;
+
+  optional int32 latency_days_weight = 5;
+
+  repeated int32 bucketed_duration_scores = 6;
+
+  optional int32 duration_weight = 7;
+
+  repeated int32 bucketed_transmission_risk_scores = 8;
+
+  optional int32 transmission_risk_weight = 9;
+}
+
+// holds all throttling representations for the given client
+// next index: 4
+message ClientThrottling {
+  reserved 1;
+  // provideDiagnosisKeys 24h throttling state
+  optional ThrottleState client_provide_diagnosis_keys_24_hour_throttle = 2;
+
+  // setDiagnosisKeysDataMapping throttling state
+  optional ThrottleState set_diagnosis_keys_data_mapping_throttle = 3;
+}
+
+// simple fixed window throttling state
+// next index: 5
+message ThrottleState {
+  reserved 3;
+
+  // latest window start time
+  optional int64 window_start = 1;
+
+  // unthrottled access count in latest window
+  optional int32 window_count = 2;
+
+  // unthrottled access with TOKEN_A count in latest window
+  optional int32 window_count_with_token_a = 4;
+}
+
+// A record representing a completed matching request and the exposure result
+// summary next index: 5
+message CompletedMatchingRequestRecord {
+  // The time in milliseconds since epoch indicating the request completion time
+  optional int64 timestamp_millis = 1 [default = 0];
+  // The number of keys involved in this request's matching process
+  optional int32 key_count = 2;
+  // The number of matched key counts found in this request
+  optional int32 matches_count = 3;
+  // Composite sha256 hash of the temporary exposure keys passed for this
+  // request
+  optional string temporary_keys_batch_hash = 4;
+}
+
+// holds configuration from setDiagnosisKeysDataMapping API call.
+// next index: 4
+message TekMetadataRecord {
+  // Mapping from diagnosisKey.daysSinceOnset to Infectiousness.
+  repeated Infectiousness days_since_onset_to_infectiousness = 1;
+
+  // Report type to default to when a TEK has no report type.
+  optional TemporaryExposureKey.ReportType report_type_when_missing = 2
+  [default = CONFIRMED_TEST];
+
+  // Returns the Infectiousness for TEKs which don't contain onset day.
+  optional Infectiousness infectiousness_when_days_since_onset_missing = 3
+  [default = INFECTIOUSNESS_STANDARD];
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/proto/contact_record.proto b/exposurenotification/src/main/proto/contact_record.proto
new file mode 100644
index 0000000..d9095b2
--- /dev/null
+++ b/exposurenotification/src/main/proto/contact_record.proto
@@ -0,0 +1,68 @@
+syntax = "proto2";
+
+package com.google.samples.exposurenotification.storage;
+
+option java_multiple_files = true;
+option java_package = "com.google.samples.exposurenotification.storage";
+
+// The record value of a sighted device in a 10 to 20 minutes window. One
+// contact record corresponds to one Rolling Proximity Id. Contact record
+// contains one or more `SightingRecord`s representing detailed signals from BLE
+// sightings.
+message ContactRecordValue {
+  repeated SightingRecord sighting_records = 1;
+
+  // Encrypted metadata received from the remote device that can only be
+  // decrypted with the right encryption keys derived from diagnosis key.
+  //
+  // This information is received in each received packet, but the encrypted
+  // metadata should be unchanged during the same rolling proximity ID window.
+  // Thus we store it here to compact the storage. However we do allow
+  // individual packet to have different metadata, in such case we'll set
+  // `SightingRecord.encrypted_metadata` which overtakes this value.
+  optional bytes encrypted_metadata = 2;
+}
+
+// The record of one BLE sighting. A sighting is defined as: at least one packet
+// is received from the remote device within a scan cycle which has a duration
+// of 4 minutes. Multiple packets received are grouped as one sighting record.
+// Next tag: 7
+message SightingRecord {
+  // The choice of uint32: 1) to save 4 bytes each record comparing to (u)int64,
+  // 2) granularity finer than seconds is not necessary. 3) MAX(int32) since
+  // epoch is not big enough so an uint is chose.
+  optional uint32 epoch_seconds = 1;
+
+  // The Received signal strength indicator of all raw BLE packets received of
+  // this sighting. Each byte in this array represent one RSSI value of a
+  // packet.
+  //
+  // This field is essentially "repeated int8 rssi_values". A valid sighting
+  // record should have a minimum size of 1.
+  optional bytes rssi_values = 2;
+
+  // Deprecated: use rssi_values.
+  optional int32 rssi = 3 [deprecated = true];
+
+  // Encrypted metadata received from the remote device that can only be
+  // decrypted with the right encryption keys derived from diagnosis key.
+  //
+  // This field shall not be set unless the AEM is changed from the beginning
+  // of the rolling proximity id validation window. When this field is
+  // unset, use `encrypted_metadata` in `ContactRecordValue`.
+  //
+  // When set, this repeated field will have a number of elements that is equal
+  // to the length of `rssi_values`. Each element is a 4-byte array per the
+  // definition of Associated Encrypted Metadata.
+  repeated bytes encrypted_metadata = 6;
+
+  // Deprecated: use encrypted_metadata.
+  optional bytes associated_encrypted_metadata = 4 [deprecated = true];
+
+  // Timestamp of the previous scan instance (potentially empty or not
+  // containing this RPI). Scan in this context is either explicitly requested
+  // (via start/stop calls) or the last result of opportunistic scanning. If
+  // there is no previous scan recorded (e.g., due to device reboot or service
+  // crashing), defaults to 0.
+  optional uint32 previous_scan_epoch_seconds = 5;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/proto/exposure_key_export.proto b/exposurenotification/src/main/proto/exposure_key_export.proto
new file mode 100644
index 0000000..edb5b5f
--- /dev/null
+++ b/exposurenotification/src/main/proto/exposure_key_export.proto
@@ -0,0 +1,129 @@
+/*
+ *
+ * NOTE:  This proto is agreed upon with Apple as well as servers that
+ *        generate EN Exposure Key export files. Do not make non-essential
+ *        changes or non-backwards compatible edits.
+ *
+ * See:
+ * https://github.com/google/exposure-notifications-server/blob/main/internal/pb/export/export.proto
+ * https://developers.google.com/android/exposure-notifications/exposure-key-file-format
+ * https://developer.apple.com/documentation/exposurenotification/setting_up_an_exposure_notification_server#3591262
+ * https://github.com/google/exposure-notifications-android/blob/master/app/src/main/java/com/google/android/apps/exposurenotification/proto/key_file.proto
+ *
+ */
+syntax = "proto2";  // Apple's framework proto libraries only support proto2
+
+package com.google.samples.exposurenotification;
+
+import "nanopb.proto";
+
+option java_package = "com.google.samples.exposurenotification";
+option java_outer_classname = "ExposureKeyExportProto";
+
+// Protobuf definition for exports of confirmed temporary exposure keys.
+//
+// The full file format is documented under "Exposure Key Export File Format
+// and Verification" at https://www.google.com/covid19/exposurenotifications/
+//
+// These files have a 16-byte, space-padded header before the protobuf data
+// starts. They will be contained in a zip archive, alongside a signature
+// file verifying the contents.
+message TemporaryExposureKeyExport {
+  // Time window of keys in this file based on arrival to server, in UTC
+  // seconds
+  optional fixed64 start_timestamp = 1;
+  optional fixed64 end_timestamp = 2;
+
+  // Region for which these keys came from (e.g., country)
+  optional string region = 3;
+
+  // E.g., Batch 2 of 10. Ordinal, 1-based numbering.
+  // Note: Not yet supported on iOS. Use values of 1 for both.
+  optional int32 batch_num = 4;
+  optional int32 batch_size = 5;
+
+  // Information about signatures
+  repeated SignatureInfo signature_infos = 6;
+
+  // The TemporaryExposureKeys for initial release of keys.
+  // Keys should be included in this list for initial release,
+  // whereas revised or revoked keys should go in revised_keys.
+  repeated TemporaryExposureKey keys = 7;
+
+  // TemporaryExposureKeys that have changed status.
+  // Keys should be included in this list if they have changed status
+  // or have been revoked.
+  repeated TemporaryExposureKey revised_keys = 8;
+}
+
+message SignatureInfo {
+  // These fields are no longer needed (confirmed with Apple)
+  // Apple read bundle id from an app's metadata, and we were
+  // always using calling app package. It's not needed in the file.
+  reserved 1, 2;
+  reserved "app_bundle_id", "android_package";
+  // Key version for rollovers
+  // Must be in character class [a-zA-Z0-9_]. E.g., 'v1'
+  optional string verification_key_version = 3;
+  // Alias with which to identify public key to be used for verification
+  // Must be in character class [a-zA-Z0-9_]
+  // For cross-compatibility with Apple, use MCC
+  // (https://en.wikipedia.org/wiki/Mobile_country_code).
+  optional string verification_key_id = 4;
+  // ASN.1 OID for Algorithm Identifier. Supported algorithms are
+  // either 1.2.840.10045.4.3.2 or 1.2.840.10045.4.3.4
+  optional string signature_algorithm = 5;
+}
+
+message TemporaryExposureKey {
+  // Key of infected user
+  optional bytes key_data = 1
+  [(nanopb).type = FT_STATIC, (nanopb).max_size = 16];
+
+  // Varying risks associated with exposure depending on type of verification
+  // Ignored by the v1.5 API when report_type is set.
+  optional int32 transmission_risk_level = 2 [deprecated = true];
+
+  // The interval number since epoch for which a key starts
+  optional int32 rolling_start_interval_number = 3;
+
+  // Increments of 10 minutes describing how long a key is valid
+  optional int32 rolling_period = 4 [default = 144];  // defaults to 24 hours
+
+  // Data type representing why this key was published.
+  enum ReportType {
+    // Never returned by the API.
+    REPORT_TYPE_UNKNOWN = 0;
+    CONFIRMED_TEST = 1;
+    CONFIRMED_CLINICAL_DIAGNOSIS = 2;
+    SELF_REPORT = 3;
+    // Reserved for future use.
+    RECURSIVE = 4;
+    // Used to revoke a key, never returned by API.
+    REVOKED = 5;
+  }
+
+  // Type of diagnosis associated with a key.
+  optional ReportType report_type = 5;
+
+  // Number of days elapsed between symptom onset and the TEK being used.
+  // E.g. 2 means TEK is 2 days after onset of symptoms.
+  optional sint32 days_since_onset_of_symptoms = 6;
+}
+
+message TEKSignatureList {
+  repeated TEKSignature signatures = 1;
+}
+
+message TEKSignature {
+  // Info about the signing key, version, algorithm, etc. Only the
+  // verification_key_id, verification_key_version, and
+  // signature_algorithm fields within signature_info are read.
+  optional SignatureInfo signature_info = 1;
+  // E.g., Batch 2 of 10 - these fields are ignored on android in favor of the
+  // batch fields within TemporaryExposureKeyExport
+  optional int32 batch_num = 2;
+  optional int32 batch_size = 3;
+  // Signature in X9.62 format (ASN.1 SEQUENCE of two INTEGER fields)
+  optional bytes signature = 4;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/proto/exposure_notification_enums.proto b/exposurenotification/src/main/proto/exposure_notification_enums.proto
new file mode 100644
index 0000000..ed56f74
--- /dev/null
+++ b/exposurenotification/src/main/proto/exposure_notification_enums.proto
@@ -0,0 +1,30 @@
+syntax = "proto2";  // Apple's framework proto libraries only support proto2
+
+package com.google.samples.exposurenotification;
+
+option java_package = "com.google.samples.exposurenotification";
+option java_outer_classname = "ExposureNotificationEnums";
+
+// The confidence value indicates where the calibration data comes from.
+// The higher value means the higher confidence.
+enum CalibrationConfidence {
+  // No calibration data, using fleet-wide as default options.
+  LOWEST_CONFIDENCE = 0;
+
+  // Using average calibration over models from manufacturer.
+  LOW_CONFIDENCE = 1;
+
+  // Using single-antenna orientation for a similar model.
+  MEDIUM_CONFIDENCE = 2;
+
+  // Using significant calibration data for this model.
+  HIGH_CONFIDENCE = 3;
+}
+
+// Infectiousness of the TEK that caused this exposure, computed from the days
+// since onset of symptoms using the daysToInfectiousnessMapping.
+enum Infectiousness {
+  INFECTIOUSNESS_NONE = 0;
+  INFECTIOUSNESS_STANDARD = 1;
+  INFECTIOUSNESS_HIGH = 2;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/proto/exposure_result.proto b/exposurenotification/src/main/proto/exposure_result.proto
new file mode 100644
index 0000000..5350c9c
--- /dev/null
+++ b/exposurenotification/src/main/proto/exposure_result.proto
@@ -0,0 +1,148 @@
+syntax = "proto2";
+
+package com.google.samples.exposurenotification.storage;
+
+import "exposure_key_export.proto";
+import "exposure_notification_enums.proto";
+import "tracing_params_record.proto";
+
+option java_multiple_files = true;
+option java_package = "com.google.samples.exposurenotification.storage";
+
+// A list of exposure matches found.
+message ExposureResult {
+  repeated ExposureRecord exposure_records = 1;
+  optional TracingParamsRecord tracing_params_record = 2;
+
+  // Day-level resolution that the exposure results were calculated for in
+  // millis since epoch.
+  optional int64 date_millis_since_epoch = 3;
+
+  // Max risk score for these exposure records.
+  optional int32 max_risk_score = 4;
+
+  // Total score for these exposure records.
+  optional int32 total_risk_score = 5;
+
+  // Attenuation durations below, between, and above the thresholds specified in
+  // the configuration as durationAtAttenuationThresholds, summed over all the
+  // records in this result.
+  repeated int32 attenuation_durations = 6;
+
+  // All ExposureWindows attributed to the diagnosis key to which this
+  // ExposureResult belongs.
+  repeated ExposureWindowProto exposure_windows = 7;
+
+  // Metadata related to the Temporary Exposure Key.
+  optional TekMetadata tek_metadata = 8;
+
+  // Number of tek_metadata#report_type is updated.
+  optional int32 report_type_transition_count = 9;
+}
+
+// Match results based on the provided tracing parameters. Directly maps to
+// the information passed back to the client when an exposure match is
+// calculated. An instance of this object represents what the system deems to
+// be an exposure based on the tracing parameters provided.
+message ExposureRecord {
+  reserved 1;
+  // Length of exposure in 5 minute increments.
+  optional int64 duration_seconds = 2;
+
+  // Signal strength attenuation, representing the weighted average of devices
+  // proximity through the exposure. This value is the advertiser's TX power
+  // minus the receiver's RSSI.
+  // The average is computed using time spent at attenuation levels as weights.
+  // The attenuation level between two scans is a simple average of measured
+  // attenuations. The first and last scans get extended by half of
+  // scanInterval. Consider:
+  //
+  // scanInterval = 6
+  // t=00, attenuation=1
+  // t=10, attenuation=4
+  // t=50, attenuation=5
+  //
+  // The resulting attenuation value is:
+  //
+  // (6/2 * 1 + (10 - 0) * 1 + (50 - 10) * 4 + 6/2 * 5) /
+  //                                         (3 + 10 + 40 +3) = 188 / 56 = 3.36
+  //
+  // The result is rounded to 3.
+  //
+  // Note: This value may be misleading, higher attenuation does not necessarily
+  // mean farther away. Phone in pocket vs hand can greatly affect this value,
+  // along with other situations that can block the signal.
+  //
+  // This value will be in the range 0-255.
+  optional int32 attenuation_value = 3;
+
+  // Risk score for this record.
+  optional int32 risk_score = 4;
+
+  // Attenuation durations below, between, and above the thresholds specified in
+  // the configuration as durationAtAttenuationThresholds.
+  repeated int32 attenuation_durations = 5;
+
+  // Transmission risk level associated with this record.
+  optional int32 transmission_risk_level = 6;
+}
+
+// Internal representation, as described in go/exposure-windows.
+message ExposureWindowProto {
+  // Exact time in seconds assigned for this exposure window.
+  optional int32 epoch_seconds = 1;
+
+  // All scans (potentially containing multiple sightings) in this window.
+  repeated ScanInstanceProto scan_instances = 2;
+
+  // Metadata related to the Temporary Exposure Key.
+  optional TekMetadata tek_metadata = 3;
+}
+
+// A single instance of BLE scan, potentially containing multiple beacon
+// sightings.
+message ScanInstanceProto {
+  // A list of all attenuations of this TEK in this scan.
+  repeated int32 attenuations = 1;
+
+  // Since the previous scan, regardless if it contained this TEK or any
+  // sightings at all.
+  optional int32 seconds_since_last_scan = 2;
+}
+
+message TekMetadata {
+  reserved 2;
+
+  // Type of diagnosis associated with a key.
+  optional TemporaryExposureKey.ReportType report_type = 1;
+
+  // Infectiousness of diagnosis associated with a key.
+  optional Infectiousness infectiousness = 3;
+
+  // Calibration confidence of diagnosis associated with a key.
+  optional CalibrationConfidence calibration_confidence = 4;
+}
+
+// Internal representation, as described in go/exposure-windows.
+message DailySummariesProto {
+  optional int32 days_since_epoch = 1;
+
+  optional ExposureSummaryDataProto confirmed_test_summary = 2;
+
+  optional ExposureSummaryDataProto confirmed_clinical_summary = 3;
+
+  optional ExposureSummaryDataProto recursive_summary = 4;
+
+  optional ExposureSummaryDataProto self_report_summary = 5;
+
+  optional ExposureSummaryDataProto day_summary = 6;
+}
+
+// Internal representation, as described in go/exposure-windows.
+message ExposureSummaryDataProto {
+  optional double maximum_score = 1;
+
+  optional double score_sum = 2;
+
+  optional double weighted_duration_sum = 3;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/proto/matching_request.proto b/exposurenotification/src/main/proto/matching_request.proto
new file mode 100644
index 0000000..8a16e1a
--- /dev/null
+++ b/exposurenotification/src/main/proto/matching_request.proto
@@ -0,0 +1,53 @@
+syntax = "proto2";
+
+package com.google.samples.exposurenotification.storage;
+
+option java_multiple_files = true;
+option java_package = "com.google.samples.exposurenotification.storage";
+
+// This message is used to store all information about a matching request. It is
+// called MatchingRequest*Proto* to avoid name conflict with the Java class
+// MatchingRequest for public surfacing.
+message MatchingRequestProto {
+  // The package name the feed is from.
+  optional string package_name = 1;
+
+  // The signature of the client app, together with package_name to uniquely
+  // identify a client.
+  optional bytes signature_hash = 2;
+
+  // The client provided string for result retrieval.
+  optional string token = 3;
+
+  // The exposure configuration in proto format.
+  optional ExposureConfigurationProto exposure_configuration = 4;
+
+  // A list of file paths that the diagnosis keys are in.
+  repeated string key_files = 5;
+
+  // The SHA-256 hash of all key files in the matching request, in base64.
+  optional string key_files_hash = 6;
+
+  // The time in milliseconds this request was received.
+  optional int64 request_time_millis = 7;
+}
+
+// A proto representation of
+// com.google.samples.exposurenotification.nearby.ExposureConfiguration.
+message ExposureConfigurationProto {
+  optional int32 minimum_risk_score = 1;
+
+  repeated int32 attenuation_scores = 2;
+  optional int32 attenuation_weight = 3;
+
+  repeated int32 days_since_last_exposure_scores = 4;
+  optional int32 days_since_last_exposure_weight = 5;
+
+  repeated int32 duration_scores = 6;
+  optional int32 duration_weight = 7;
+
+  repeated int32 transmission_risk_scores = 8;
+  optional int32 transmission_risk_weight = 9;
+
+  repeated int32 duration_at_attenuation_thresholds = 10;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/proto/temporary_exposure_key_properties.proto b/exposurenotification/src/main/proto/temporary_exposure_key_properties.proto
new file mode 100644
index 0000000..354ff04
--- /dev/null
+++ b/exposurenotification/src/main/proto/temporary_exposure_key_properties.proto
@@ -0,0 +1,15 @@
+syntax = "proto2";
+
+package com.google.samples.exposurenotification.storage;
+
+option java_multiple_files = true;
+option java_package = "com.google.samples.exposurenotification.storage";
+
+// Metadata corresponding to a temporary exposure key.
+message TemporaryExposureKeyProperties {
+  // The number of intervals that the key is valid for from its start interval.
+  optional int32 rolling_period = 1 [default = 144];
+
+  // The starting interval number for the given temporary exposure key
+  optional int32 rolling_start_interval_number = 2;
+}
\ No newline at end of file
diff --git a/exposurenotification/src/main/proto/tracing_params_record.proto b/exposurenotification/src/main/proto/tracing_params_record.proto
new file mode 100644
index 0000000..42df8ff
--- /dev/null
+++ b/exposurenotification/src/main/proto/tracing_params_record.proto
@@ -0,0 +1,24 @@
+syntax = "proto2";
+
+package com.google.samples.exposurenotification.storage;
+
+option java_multiple_files = true;
+option java_package = "com.google.samples.exposurenotification.storage";
+
+// The parameters used when tracing bluetooth signals for exposure.
+message TracingParamsRecord {
+  // Minimum consecutive sightings in duration required to be considered a
+  // contact, compared with a bucketized duration calculation.
+  optional int64 min_exposure_bucketized_duration_seconds = 1;
+  // Maximum duration gap between two sightings to be considered continuous
+  // contact with another device.
+  optional int64 max_interpolation_duration_seconds = 2;
+  // Minimum ble signal strength indicator value that must be reached by at
+  // least one sighting in a series of sightings that are being considered for
+  // an exposure
+  optional int32 min_attenuation_value = 3;
+  // Bluetooth token scanning interval.
+  optional int32 scan_interval_seconds = 4;
+  // Whether to interpolate attenuation when calculating duration at thresholds.
+  optional bool interpolation_enabled = 5;
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..413b4e3
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,37 @@
+#
+# Copyright 2020 Google LLC
+#
+# 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.
+#
+
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a1f4b4a
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,22 @@
+#
+# Copyright 2020 Google LLC
+#
+# 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.
+#
+
+#Mon Jun 15 11:27:16 PDT 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+  cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/images/Crypto-Flowchart.png b/images/Crypto-Flowchart.png
new file mode 100644
index 0000000..acafa41
Binary files /dev/null and b/images/Crypto-Flowchart.png differ
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..3ef4b3a
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+include ':exposurenotification'
+include ':app'
+rootProject.name = "Exposure Notifications Snippets"
diff --git a/third_party/boringssl/.dockerignore b/third_party/boringssl/.dockerignore
new file mode 100644
index 0000000..a4cc053
--- /dev/null
+++ b/third_party/boringssl/.dockerignore
@@ -0,0 +1,5 @@
+armeabi-v7a/
+arm64-v8a/
+x86/
+x86_64/
+src/
diff --git a/third_party/boringssl/.gitignore b/third_party/boringssl/.gitignore
new file mode 100644
index 0000000..a4cc053
--- /dev/null
+++ b/third_party/boringssl/.gitignore
@@ -0,0 +1,5 @@
+armeabi-v7a/
+arm64-v8a/
+x86/
+x86_64/
+src/
diff --git a/third_party/boringssl/Dockerfile b/third_party/boringssl/Dockerfile
new file mode 100644
index 0000000..7d43074
--- /dev/null
+++ b/third_party/boringssl/Dockerfile
@@ -0,0 +1,52 @@
+# Copyright 2020 Google LLC
+#
+# 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.
+#
+FROM golang
+
+# Library versions
+ARG BORINGSSL_COMMIT_ID
+ENV ANDROID_NDK_VERSION r21d
+
+# Install dependencies
+RUN apt-get update && apt-get install --no-install-recommends -yq \
+    ca-certificates cmake git unzip ninja-build && \
+    rm -rf /var/lib/apt/lists/*
+
+# Download the Android NDK
+ENV ANDROID_NDK_ROOT /go/android-ndk-${ANDROID_NDK_VERSION}
+RUN wget -qq https://dl.google.com/android/repository/android-ndk-${ANDROID_NDK_VERSION}-linux-x86_64.zip -O /go/android-ndk.zip && \
+    unzip -qq /go/android-ndk.zip -d /go && \
+    rm /go/android-ndk.zip
+
+# Clone the BoringSSL source
+ENV BORINGSSL_SRC /src/boringssl
+RUN git clone https://boringssl.googlesource.com/boringssl ${BORINGSSL_SRC}
+
+# Check out the appropriate commit for reproducibility
+WORKDIR ${BORINGSSL_SRC}
+RUN git checkout ${BORINGSSL_COMMIT_ID}
+
+# Image arguments used to determine the target Android ABI
+ARG ANDROID_ABI
+RUN echo "Building BoringSSL for Android ${ANDROID_ABI}"
+
+# Cross compile the Android binaries
+WORKDIR /opt/boringssl/android/${ANDROID_ABI}
+RUN cmake \
+        -DANDROID_ABI=${ANDROID_ABI} \
+        -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake \
+        -DANDROID_NATIVE_API_LEVEL=16 \
+        -GNinja ${BORINGSSL_SRC} && \
+    ninja all
+
diff --git a/third_party/boringssl/build-android.sh b/third_party/boringssl/build-android.sh
new file mode 100755
index 0000000..1b73d04
--- /dev/null
+++ b/third_party/boringssl/build-android.sh
@@ -0,0 +1,79 @@
+#!/bin/sh
+# Copyright 2020 Google LLC
+#
+# 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.
+#
+
+readonly BORINGSSL_IMAGE="boringssl"
+readonly BORINGSSL_COMMIT_ID="bc2480510960a77bea24edc64fcb089aca103940"
+
+#######################################
+# Cross-compiles BoringSSL for Android using the Docker image in this folder
+# Globals:
+#   BORINGSSL_IMAGE: Name of the docker image used to build BoringSSL
+# Arguments:
+#   android_abi: ABI flavour, e.g. armeabi-v7a.
+#######################################
+boringssl_android_build() {
+
+  # Process arguments
+  local -r android_abi=$1
+
+  # Build the Docker image for the specific Android ABI
+  local -r dockerfile_path=`dirname "$0"`
+  docker build --rm \
+    --build-arg ANDROID_ABI=$android_abi \
+    --build-arg BORINGSSL_COMMIT_ID=$BORINGSSL_COMMIT_ID \
+    --tag $BORINGSSL_IMAGE:$android_abi \
+    $dockerfile_path
+}
+
+#######################################
+# Copies the build artifacts for the given Android ABI flavour into the
+# provided destination folder.
+# Globals:
+#   BORINGSSL_IMAGE: Name of the docker image used to build BoringSSL
+# Arguments:
+#   android_abi: ABI flavour, e.g. armeabi-v7a.
+#   dest: Destination folder to place artifacts.
+#######################################
+copy_build_artifacts() {
+
+  # Process arguments
+  local -r android_abi=$1
+  local -r dest=$2
+
+  # Create a Docker image and tar the build artifacts into local storage
+  local -r image_id=$(docker create ${BORINGSSL_IMAGE}:${android_abi})
+  local -r output_tar="/tmp/boringssl-${android_abi}.tar"
+  docker cp ${image_id}:"/opt/boringssl/android/${android_abi}" - > $output_tar
+  docker rm -v $image_id
+
+  # Extract the tar into the desired destination folder
+  mkdir -p $dest
+  tar -xvf $output_tar -C $dest
+}
+
+# Determine the location of the base directory
+readonly BASE_DIR=$(dirname "$0")
+
+# Download the source so we can reference the header files
+git clone https://boringssl.googlesource.com/boringssl "$BASE_DIR"/src
+git --git-dir "$BASE_DIR"/src/.git checkout ${BORINGSSL_COMMIT_ID}
+
+# Compile each ABI and output artifacts into a subfolder
+for flavour in "armeabi-v7a" "arm64-v8a" "x86" "x86_64"
+do
+    boringssl_android_build $flavour
+    copy_build_artifacts $flavour "$BASE_DIR"
+done
diff --git a/third_party/nanopb/.gitignore b/third_party/nanopb/.gitignore
new file mode 100644
index 0000000..b160582
--- /dev/null
+++ b/third_party/nanopb/.gitignore
@@ -0,0 +1,3 @@
+linux/
+macosx/
+windows/
diff --git a/third_party/nanopb/download.sh b/third_party/nanopb/download.sh
new file mode 100644
index 0000000..173e521
--- /dev/null
+++ b/third_party/nanopb/download.sh
@@ -0,0 +1,39 @@
+#!/bin/sh
+# Copyright 2020 Google LLC
+#
+# 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.
+#
+
+#######################################
+# Download and extract the nanopb precompiled binaries for the given platform
+# Arguments:
+#   dest: destination folder for the binaries
+#   platform: linux, macosx or windows
+#######################################
+download_nanopb() {
+
+  # Process arguments
+  local -r dest=$1
+  local -r platform=$2
+
+  # Download appropriate binaries
+  local -r output="${dest}/${platform}"
+  local -r tar_file="${dest}/${platform}.tar.gz"
+  wget -c "https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.2-${platform}-x86.tar.gz" -O "$tar_file"
+  mkdir -p "$output" && tar xvf "$tar_file" -C "$output" --strip-components 1 && rm -rf "$tar_file"
+}
+
+readonly base_path=`dirname "$0"`
+download_nanopb "$base_path" linux
+download_nanopb "$base_path" macosx
+

Fixed code: name: Android CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:

  test:
    name: Run Unit Tests
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK 9
        uses: actions/setup-java@v1
        with:
          java-version: 9

      - name: Install dependencies
        run: |
          bash ./third_party/nanopb/download.sh
          bash ./third_party/boringssl/build-android.sh

      - name: Gradle build
        run: bash ./gradlew -Pci --no-parallel build --stacktrace

      - name: Gradle test
        run: bash ./gradlew -Pci --no-parallel test --stacktrace
.classpath
.gradle
**/.idea/**
!**/.idea/codeStyleSettings.xml
!**/.idea/copyright
!**/.idea/codeStyles
!.idea/codeStyles/Project.xml
!.idea/codeStyles/codeStyleConfig.xml
!.idea/copyright/AndroidCopyright.xml
!.idea/copyright/profiles_settings.xml
!.idea/inspectionProfiles/Project_Default.xml
!.idea/vcs.xml
.project
.settings/
project.properties
local.properties
**/bin
**/gen
*.iml
**/out
**/build
**/.cxx
<component name="ProjectCodeStyleConfiguration">
  <code_scheme name="Project" version="173">
    <JavaCodeStyleSettings>
      <option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
    </JavaCodeStyleSettings>
    <JetCodeStyleSettings>
      <option name="PACKAGES_TO_USE_STAR_IMPORTS">
        <value />
      </option>
      <option name="PACKAGES_IMPORT_LAYOUT">
        <value>
          <package name="" alias="false" withSubpackages="true" />
          <package name="java" alias="false" withSubpackages="true" />
          <package name="javax" alias="false" withSubpackages="true" />
          <package name="kotlin" alias="false" withSubpackages="true" />
          <package name="" alias="true" withSubpackages="true" />
        </value>
      </option>
      <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
      <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
    </JetCodeStyleSettings>
    <codeStyleSettings language="JAVA">
      <option name="WRAP_ON_TYPING" value="1" />
    </codeStyleSettings>
    <codeStyleSettings language="Markdown">
      <option name="WRAP_ON_TYPING" value="1" />
    </codeStyleSettings>
    <codeStyleSettings language="ObjectiveC">
      <indentOptions>
        <option name="INDENT_SIZE" value="2" />
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
        <option name="TAB_SIZE" value="2" />
      </indentOptions>
    </codeStyleSettings>
    <codeStyleSettings language="XML">
      <indentOptions>
        <option name="CONTINUATION_INDENT_SIZE" value="4" />
      </indentOptions>
      <arrangement>
        <rules>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>xmlns:android</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>xmlns:.*</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:id</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*:name</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>name</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>style</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>^$</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                </AND>
              </match>
              <order>ANDROID_ATTRIBUTE_ORDER</order>
            </rule>
          </section>
          <section>
            <rule>
              <match>
                <AND>
                  <NAME>.*</NAME>
                  <XML_ATTRIBUTE />
                  <XML_NAMESPACE>.*</XML_NAMESPACE>
                </AND>
              </match>
              <order>BY_NAME</order>
            </rule>
          </section>
        </rules>
      </arrangement>
    </codeStyleSettings>
    <codeStyleSettings language="kotlin">
      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
    </codeStyleSettings>
  </code_scheme>
</component>
<component name="ProjectCodeStyleConfiguration">
  <state>
    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
  </state>
</component>
<component name="CopyrightManager">
  <settings>
    <module2copyright>
      <element module="All" copyright="Google" />
    </module2copyright>
  </settings>
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>
# Building

This source in this project does not create a usable product, but is provided as a reference that
can be used with the following specifications:

- [Bluetooth Specification](https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf)
- [Cryptography Specification](https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf)

It is nonetheless possible to compile this code in order to run the various test cases provided.

## Dependencies

This project requires Java 9 to build the `exposurenotification` subproject. You can check the Java
version by running `java -version`. If you want to be able to build from Android Studio, you can
change the JDK location under `File > Project Structure > JDK Location`.

This also project depends on a few third-party, open source libraries:

* [BoringSSL](https://boringssl.googlesource.com/boringssl/)
* [NaoPB](https://jpa.kapsi.fi/nanopb/download/)

It is helpful to place both of these dependencies in a common directory. For convenience, shell
scripts which download and build these third-party libraries are provided which require Docker to
be installed to run:

* [Docker Engine](https://docs.docker.com/engine/install/)

## Building BoringSSL

### Using Docker

To build cross-compile BoringSSL for Android using Docker, run the following bash script:
```bash
bash ./third_party/boringssl/build-android.sh
```

### Using Native Toolchain

To build BoringSSL, clone the repo and build it according to the steps described in the included
`BUILDING.md` file.

NOTE: This project is set to build for `arm64-v8a` and `x86`, in order to support testing on
modern Android devices, as well as emulators. As such, it expects the output from building
BoringSSL to be in a directory named `build-${ANDROID_ABI}`. This means BoringSSL must be built
twice. Once with the setting `-DANDROID_ABI=x86`, for Android emulators, and a second time with
 `-DANDROID_ABI=arm64-v8a` for physical devices.

Depending on where you place the resulting build artifacts, you may need to change the
`boringSslRoot` variable in the `build.gradle` file.

## Downloading NanoPB

The `build.gradle` file expects a binary distribution of NanoPB to be present under the folder
`./third_party/nanopb/${platform}`. You can run the provided bash script to download it:
```bash
bash ./third_party/nanopb/download.sh
```

Alternatively, you can manually download a binary distribution of NanoPB from the provided link
above. Extract it under the folder described above, or configure `build.gradle` accordingly.


At this point the project should be able to build and the tests should run.
# How to Contribute

We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.

## Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.

## Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.

## Community Guidelines

This project follows [Google's Open Source Community
Guidelines](https://opensource.google.com/conduct/).
# Exposure Notifications Cryptography

This document contains snippets of code from the sample reference implementation, occasionally in pseudocode, for the
Implementation of the [Exposure Notification Cryptography
Specification](https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf).

Many of the snippets included in this document have been simplified from their actual
implementation. Constants, select parameters, and member variables have been inlined for brevity.
The actual implementation can be seen by inspecting the classes referenced.

Because this document follows the ordering of the specification, we recommend you read them
together.

## Cryptographic Overview

The following chart  may be helpful for better understanding how the various parts of the system
fit together.

<img src="images/Crypto-Flowchart.png">

## ENIntervalNumber

This function provides a number for each 10 minute time window that’s shared between all devices
participating in the protocol. These time windows are derived from timestamps in Unix Epoch Time.

```java
private static int getCurrentEnIntervalNumber(Instant instant) {
    return (int) (instant.toEpochMilli() / MINUTES.toMillis(10));
}
```

`ENIntervalNumber` is part of
`com.google.samples.exposurenotification.ble.advertising.RollingProximityIdManager`.

## TEKRollingPeriod

The TEKRollingPeriod is the duration for which a Temporary Exposure Key is valid (in multiples of 10
minutes). In our current implementation, the TEKRollingPeriod is  144, achieving a key validity of 24 hours. 

The rolling period is part of the 
`com.google.samples.exposurenotification.nearby.TemporaryExposureKey` class and is set by the
`TemporaryExposureKey.Builder#setRollingPeriod` method.

## Temporary Exposure Key

Temporary Exposure Keys (TEK) are not created explicitly, but rather implicitly as required. This happens due to one of two reasons:

* This is the very first TEK being generated
* The current TEK has expired

This check is performed by first calling `RollingProximityIdManager#getExistingKey`. If this call returns `null`, then a new key is generated.

If a TEK is returned, the code compares the current rolling period with the last valid rolling period for the key. If that check indicates the key has expired (the current period is greater than the last rolling period), a new TEK is generated.

Pseudocode of for this check follows:

```java
var temporaryExposureKey = getCurrentTEK();
if (temporaryExposureKey == null ||
    currentEnIntervalNumber > temporaryExposureKey.getlastValidRollingPeriod()) {
    temporaryExposureKey = TemporaryExposureKeyGenerator.generateKey();
}
```

The implementation of of `getCurrentTEK()` is provided by `RollingProximityIdManager#getExistingKey` and the value of `currentEnIntervalNumber` is calculated by the function `RollingProximityIdManager#getCurrentEnIntervalNumber`.

The key data is generated from a cryptographic random number generator so there is no link between TEKs. The method is in
`com.google.samples.exposurenotification.data.generator.TemporaryExposureKeyGenerator`

```java
public static TemporaryExposureKey generateKey(
        int rollingStartIntervalNumber, int rollingPeriod) {
    SecureRandom random = new SecureRandom();
    byte[] key = new byte[16];
    random.nextBytes(key);
    // ...
}
```

## Rolling Proximity Identifier Key

The Rolling Proximity Identifier Key (RPIK) is derived from the Temporary Exposure Key and is
used in order to create the Rolling Proximity Identifiers.

The key is generated in
`com.google.samples.exposurenotification.crypto.KeyDerivation` via
`com.google.samples.exposurenotification.data.generator.RollingProximityIdGeneratorBase#generateRpiKey`.

```java
static byte[] generateRpiKey(byte[] temporaryExposureKey) {
    // RPIK = HKDF(tek, NULL, UTF8("EN-RPIK"), 16).
    return KeyDerivation.hkdfSha256(
            temporaryExposureKey,
            /* inputSalt =*/ null,
            "EN-RPIK".getBytes(UTF_8),
            16);
}
```

```java
public static byte[] hkdfSha256(
        Mac mac, byte[] inputKeyingMaterial,
        @Nullable byte[] inputSalt,
        byte[] info,
        int length) throws CryptoException {

    byte[] salt = (inputSalt == null || inputSalt.length == 0) ? new byte[HASH_LENGTH] : inputSalt;
    try {
        mac = Mac.getInstance("HmacSHA256");
        return hkdfSha256ExpandLength16(mac, hkdfSha256Extract(mac, inputKeyingMaterial, salt), info);
    } catch (InvalidKeyException e) {
        throw new CryptoException(e);
    }
}
```

The method `hkdfSha256ExpandLength16` is implemented in the same class:

```java
private static byte[] hkdfSha256ExpandLength16(Mac mac, byte[] pseudoRandomKey, byte[] info)
        throws InvalidKeyException {
    // For length being 16 cases, counter always equals to 0x01.
    byte[] counter = {0x01};
    mac.init(new SecretKeySpec(pseudoRandomKey, ALGORITHM_NAME));
    mac.update(info);

    return Arrays.copyOf(mac.doFinal(counter), HKDF_OUTPUT_LENGTH);
}
```

## Rolling Proximity Identifier

Rolling Proximity Identifiers (RPIs) are short-lived identifiers (on average 15 minutes) that are broadcast in
Bluetooth payloads. The RPIs are generated every 10 minutes, and, because of how Android
BLE advertising works, each time it's started the system will also generate a new random BLE MAC address.

The code to generate an RPI begins in 
`com.google.samples.exposurenotification.data.generator.RollingProximityIdGeneratorBase` in the
`generateId` method:

```java
public RollingProximityId generateId(int intervalNumber) throws CryptoException {
    return new RollingProximityId(
            encryptor.encrypt(generatePaddedData(intervalNumber, "EN-RPI")));
}
```

`generatePaddedData` creates a `ByteBuffer` and calls through to the method `addPaddedData`:

```java
private static ByteBuffer addPaddedData(int intervalNumber, byte[] aesPadding) {
    // AES_BLOCK_SIZE = 16 (bytes)
    ByteBuffer buffer = ByteBuffer.allocate(AES_BLOCK_SIZE).order(ByteOrder.LITTLE_ENDIAN);
    buffer
            .put(aesPadding)
            .position(buffer.position() + AES_BLOCK_SIZE - aesPadding.length - Integer.BYTES);
    buffer.putInt(intervalNumber);
    return buffer;
}
```

The encryption to generate a new RPI from a TEK seed is handled by the class
`com.google.samples.exposurenotification.crypto.AesEcbEncryptor` in the `encrypt` method:

```java
public byte[] encrypt(byte[] data) throws CryptoException {
    try {
        Cipher cipher = new AesEcbEncryptor(Cipher.getInstance("AES/ECB/NoPadding"));
        return cipher.doFinal(data);
    } catch (BadPaddingException | IllegalBlockSizeException e) {
        throw new CryptoException(e);
    }
}
```

## Associated Encrypted Metadata Key

The Associated Encrypted Metadata Keys are derived from the Temporary Exposure Keys in order to
encrypt additional metadata.

The key is generated by the 
`com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataHelper` class:

```java
public static byte[] generateAemKey(byte[] temporaryExposureKey) {
    // AEMK = HKDF(tek, NULL, UTF8("EN-AEMK"), 16).
    return KeyDerivation.hkdfSha256(
            temporaryExposureKey,
            /* inputSalt =*/ null,
            "EN-AEMK".getBytes(UTF_8),
            16);
}
```

The `KeyDerivation.hkdfSha256` method is the same function used earlier to generate the [Rolling
Proximity Identifier Key](#Rolling-Proximity-Identifier-Key).

## Associated Encrypted Metadata

The Associated Encrypted Metadata is data encrypted along with the Rolling Proximity Identifier,
and can only be decrypted later if the user broadcasting it tested positive and reveals their
Temporary Exposure Key.

The details of the metadata itself are described in the Advertising Payload section of the
[Bluetooth Specification](https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf).

The encryption is performed by the 
`com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataHelper` class:

```java
public static byte[] encryptOrDecrypt(
        byte[] aemKey, byte[] rollingProximityId, byte[] bluetoothMetadataAsBytes)
        throws CryptoException {
    return AesCtrEncryptor.aesCtr(aemKey, rollingProximityId, bluetoothMetadataAsBytes);
}
```

The encryption of the metadata itself is performed with an AES-128 block cipher in Counter Mode with the rolling
proximity ID as the IV. 

```java
public static byte[] aesCtr(byte[] key, byte[] iv, byte[] data) throws CryptoException {
    try {
        Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));

        return cipher.doFinal(data);
    } catch (NoSuchPaddingException
            | NoSuchAlgorithmException
            | InvalidAlgorithmParameterException
            | InvalidKeyException
            | BadPaddingException
            | IllegalBlockSizeException e) {
        throw new CryptoException(e);
    }
}
```
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2014 The Android Open Source Project

   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.
# Exposure Notifications API

This repository contains a snapshot of code from Google Play Services' [Exposure Notifications
 module][1].

## Key Features

There are a number of features in this source set, including abstrations and JNI. The following sections provide key features with pointers to the source code.

### BLE MAC and RPI Rotation

Code: [`com.google.samples.exposurenotification.ble.advertising.BleAdvertiser#startAdvertising`][2]

Bluetooth Low Energy (BLE) MAC addresses rotate on average every 15 minutes to prevent remote location tracking that could be accomplished by tying together observations of a fixed MAC address.

In order to best protect user privacy, the Exposure Notifications framework ensures that Rolling
Proximity Identifiers (RPIs) are never rotated without also having a corresponding change of the
Bluetooth MAC address. For more details, see the full [Bluetooth spec][3].

Because Android doesn't have a callback to notify an application that the Bluetooth MAC address
is changing (or has changed), this is handled by explicitly stopping and restarting advertising
whenever a new RPI is generated.

Since there isn't any callback, it is possible for the Bluetooth MAC address to rotate
before a new RPI is generated. In this case the following would happen:

|Time | Bluetooth MAC | RPI |
|-----|---------------|-----|
| :00 | 00:00:00:01   | AAA |
| :09 | 00:00:00:02   | AAA |
| :10 | 00:00:00:03   | BBB |

The risk posed by this is minimal, since even though an observer may be able to tie the MAC
addresses `00:00:00:01` and `00:00:00:02` together using the common RPI, the duration of both
MAC addresses is no longer than a single RPI period (~10 minutes). When the RPI rotates, the
MAC address rotates again, making it difficult to track the association of either the MAC
address or RPI to a common device.

### Rolling Proximity Identifier Generation

Code: [`com.google.samples.exposurenotification.ble.advertising.RollingProximityIdManager#getCurrentRollingProximityId`](exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/RollingProximityIdManager.java)
Code: [`com.google.samples.exposurenotification.data.generator.TemporaryExposureKeyGenerator`](exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/TemporaryExposureKeyGenerator.java)

Rolling Proximity Identifiers (RPIs) are generated from a Temporary Exposure Key (TEK) based on the
[Exposure Notification Cryptography Specification][4].

For more information about TEKs and RPIs, see [Exposure Notifications Cryptography](CRYPTO.md#Temporary-Exposure-Key)

### Associated Encrypted Metadata

Code: [`com.google.samples.exposurenotification.ble.advertising.BleAdvertisementGenerator`](exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertisementGenerator.java)
Code: [`com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataHelper`](exposurenotification/src/main/java/com/google/samples/exposurenotification/data/generator/AssociatedEncryptedMetadataHelper.java)

Associated Encrypted Metadata is generated by `BleAdvertisementGenerator#generatePacket` based on the
[Exposure Notification Cryptography Specification][4]. This 
uses `BluetoothMetadata#create` to create the metadata itself.

The encryption, decryption, and generation of keys is handled by the
class `AssociatedEncryptedMetadataHelper`.

For more information about Associated Encrypted Metadata, see [Exposure Notifications Cryptography](CRYPTO.md#Associated-Encrypted-Metadata)

### Key Matching

Code: [`com.google.samples.exposurenotification.matching.MatchingJni`](exposurenotification/src/main/java/com/google/samples/exposurenotification/matching/MatchingJni.java)

The core process of Key Matching is started via `MatchingJni`. The class then calls into native C++ code to improve performance by avoiding Java binder calls for the many crypto operations needed.

A `MatchingJni` object is initialized with a list of scanned RPIs from the past 14 days. The caller can pass a list of Diagnosis Key files, provided by the Healthcare Authority app, to the method `MatchingJni#matching`. This method produces a list of `TemporaryExposureKey`s corresponding to the Diagnosis Keys that the device was exposed to.

[1]: https://developers.google.com/android/exposure-notifications/exposure-notifications-api
[2]: exposurenotification/src/main/java/com/google/samples/exposurenotification/ble/advertising/BleAdvertiser.java
[3]: https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf
[4]: https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf


/build
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "30.0.1"
    ndkVersion "21.3.6528147"

    defaultConfig {
        applicationId "com.google.samples.ensnippets"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14"
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

dependencies {
    implementation project(path: ':exposurenotification')

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    testImplementation 'junit:junit:4.13'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.ensnippets

import androidx.core.util.Supplier
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey
import com.google.samples.exposurenotification.ble.advertising.RollingProximityIdManager
import com.google.samples.exposurenotification.ble.interfaces.BleDatabaseWriter
import com.google.samples.exposurenotification.features.ContactTracingFeature
import com.google.samples.exposurenotification.matching.MatchingJni
import com.google.samples.exposurenotification.storage.SelfTemporaryExposureKeyDataStore
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import java.io.FileOutputStream
import java.math.BigInteger
import java.util.concurrent.TimeUnit

/**
 * Instrumented test, which will execute on an Android device.
 *
 * See [testing documentation](http://d.android.com/tools/testing).
 */
@RunWith(AndroidJUnit4::class)
class RoundTripTests {
    private lateinit var exportKeyFile: File

    @Before
    fun setup() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        appContext.resources.openRawResource(R.raw.export).use { inputStream ->
            exportKeyFile = File(appContext.filesDir, "export.bin")
            FileOutputStream(exportKeyFile).use { fileOutputStream ->
                inputStream.copyTo(fileOutputStream)
            }
        }
    }

    @After
    fun teardown() {
        if (exportKeyFile.exists()) exportKeyFile.delete()
    }

    /**
     * This is a test that shows how to call the native matching code. This test assumes that the
     * ZIP file of diagnosis keys has been verified and the diagnosis keys have been extracted
     * to the file referenced by [exportKeyFile].
     */
    @Test
    fun testGenerateAndMatchRPI() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext

        // Required parameter, but is unused in this test.
        val testBleDatabaseWriter = TestBleDatabaseWriter()

        // In order to match a key provided in the file, we've hardcoded one of the provided
        // diagnosis keys here (0x2b714d3ec93c14e5d345003e59027aea).
        //
        // We inject this key into a SelfTemporaryExposureKeyDataStore and then use the code
        // that generates Rolling Proximity Ids (RPIs) to create an RPI from the diagnosis key
        // above.
        val testDataStore =
            TestSelfTemporaryExposureKeyDataStore("2b714d3ec93c14e5d345003e59027aea", 2646801)
        val rpiManager = RollingProximityIdManager(
            testDataStore,
            testBleDatabaseWriter,
            Supplier { rollingPeriodStartToMillis(2646801) }
        )

        // Here we use the RPI manager created above to get an RPI
        val seenRpi = rpiManager.currentRollingProximityId.get()

        // We take this key and say it's a key that we saw during scanning and provide that
        // to the matching code.
        val bleScanResults = arrayOf(seenRpi)
        val matchingJni = MatchingJni(appContext, bleScanResults)

        // We then call through and provide the diagnosis keys file we extracted
        val results = matchingJni.matching(listOf(exportKeyFile.toString()))

        // Because we generated an RPI from one of the diagnosis keys in the test data, we
        // expect that we have seen exactly 1 result (since we generated one RPI)
        Assert.assertEquals(results.size, 1)

        // We check the actual results and make sure that the key that's matched is the
        // one that was generated above.
        // (This also ensures that the native code which generates RPIs from a diagnosis key
        // creates the same keys as the Java code which generates them from a temporary
        // exposure key.)
        results.forEach { matchedKey ->
            Assert.assertArrayEquals(
                matchedKey.keyData,
                keyStringToByteArray("2b714d3ec93c14e5d345003e59027aea")
            )
        }
    }

    private fun rollingPeriodStartToMillis(rollingPeriod: Int) =
        rollingPeriod *
                TimeUnit.MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes().toLong())
}

// The rolling period _could_ be shorter if we wanted to expire it early (so that we could, for
// example, share a positive diagnosis ASAP. For testing we just set it to 144 (24 hours).
private const val TEST_ROLLING_PERIOD = 144

class TestSelfTemporaryExposureKeyDataStore(
    hexKey: String,
    rollingPeriodStart: Int
) : SelfTemporaryExposureKeyDataStore() {

    init {
        val testKey = TemporaryExposureKey.TemporaryExposureKeyBuilder()
            .setKeyData(keyStringToByteArray(hexKey))
            .setRollingStartIntervalNumber(rollingPeriodStart)
            .setRollingPeriod(TEST_ROLLING_PERIOD)
            .build()
        putKey(testKey)
    }
}

class TestBleDatabaseWriter : BleDatabaseWriter {
    override fun writeNewTemporaryExposureKey(temporaryExposureKey: TemporaryExposureKey?) {
        // Nothing to store for the test.
    }

    override fun writeBleSighting(
        rollingProximityId: ByteArray?,
        rssi: Int,
        associatedEncryptedMetadata: ByteArray?,
        previousScanEpochSeconds: Int
    ) {
        // Nothing needed for testing.
    }
}

/**
 * Helper method to take a hex string (key) and convert it to a byte array.
 */
private fun keyStringToByteArray(key: String): ByteArray = BigInteger(key, 16).toByteArray()
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.ensnippets

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.google.samples.exposurenotification.matching.ProvideDiagnosisKeys
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
import java.io.FileOutputStream

/** Sample app package name */
private const val SAMPLE_PACKAGE = "com.google.android.apps.exposurenotification"

@RunWith(AndroidJUnit4::class)
class SignatureVerificationTests {
    private lateinit var diagnosisKeyFile: File

    @Before
    fun setup() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        appContext.resources.openRawResource(R.raw.sample_diagnosis_key_file).use { inputStream ->
            diagnosisKeyFile = File(appContext.filesDir, "export.zip")
            FileOutputStream(diagnosisKeyFile).use { fileOutputStream ->
                inputStream.copyTo(fileOutputStream)
            }
        }

        val targetFolder = File(appContext.filesDir, "en_diagnosis_keys")
        targetFolder.deleteRecursively()
    }

    @After
    fun teardown() {
        if (diagnosisKeyFile.exists()) diagnosisKeyFile.delete()
    }

    @Test
    fun testVerifyDiagnosisKeysZipFile() {
        // Context of the app under test.
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        val provideDiagnosisKeys = ProvideDiagnosisKeys(appContext)

        assertTrue(provideDiagnosisKeys.verify(SAMPLE_PACKAGE, diagnosisKeyFile))
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data.generator;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.crypto.AesEcbEncryptor;
import com.google.samples.exposurenotification.data.DayNumber;
import com.google.samples.exposurenotification.data.GeneratedRollingProximityId;
import com.google.samples.exposurenotification.data.RollingProximityId;
import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
import com.google.samples.exposurenotification.data.generator.RollingProximityIdGeneratorBase.PaddedDataCache;
import com.google.samples.exposurenotification.features.ContactTracingFeature;
import com.google.samples.exposurenotification.testing.TestVectors;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.List;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

@RunWith(AndroidJUnit4.class)
public class RollingProximityIdGeneratorTest {

    @Test
    public void generateId_usingTestVector() throws Exception {
        byte[] temporaryTracingKey = TestVectors.get_TEMPORARY_TRACING_KEY();
        TemporaryExposureKey temporaryExposureKey =
                new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                        .setRollingStartIntervalNumber(0)
                        .setKeyData(temporaryTracingKey)
                        .build();
        int ctIntervalNumber = TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY;
        RollingProximityIdGenerator generator =
                new RollingProximityIdGenerator(
                        temporaryExposureKey.getKeyData(),
                        temporaryExposureKey.getRollingStartIntervalNumber(),
                        TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey),
                        ContactTracingFeature.rollingProximityIdKeySizeBytes(),
                        ContactTracingFeature.rpikHkdfInfoString(),
                        ContactTracingFeature.rpidAesPaddedString());
        for (TestVectors.AdvertisedData element : TestVectors.ADVERTISED_DATA) {
            RollingProximityId generatedId = generator.generateId(ctIntervalNumber);
            assertArrayEquals(generatedId.get(), element.get_RPI());
            ctIntervalNumber++;
        }
    }

    @Test
    public void testGenerateIds_generatesCorrectIds() throws Exception {
        TemporaryExposureKey temporaryExposureKey =
                new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                        .setRollingStartIntervalNumber(TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY)
                        .setRollingPeriod(TestVectors.ADVERTISED_DATA.size())
                        .setKeyData(TestVectors.get_TEMPORARY_TRACING_KEY())
                        .build();

        List<GeneratedRollingProximityId> generatedIds =
                new RollingProximityIdGenerator(
                        temporaryExposureKey.getKeyData(),
                        temporaryExposureKey.getRollingStartIntervalNumber(),
                        TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey),
                        ContactTracingFeature.rollingProximityIdKeySizeBytes(),
                        ContactTracingFeature.rpikHkdfInfoString(),
                        ContactTracingFeature.rpidAesPaddedString())
                        .generateIds(
                                new byte
                                        [ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod()
                                        * ContactTracingFeature.contactIdLength()]);

        assertEquals(generatedIds.size(), TestVectors.ADVERTISED_DATA.size());
        for (int i = 0; i < TestVectors.ADVERTISED_DATA.size(); i++) {
            assertArrayEquals(generatedIds.get(i).rollingProximityId().get(),
                    TestVectors.ADVERTISED_DATA.get(i).get_RPI());
        }
    }

    // Runs the RPIK test vector on the spec:
    @Test
    public void generateRpiKey_usingTestVector() throws Exception {
        byte[] rpiKey =
                RollingProximityIdGenerator.generateRpiKey(
                        TestVectors.get_TEMPORARY_TRACING_KEY(),
                        TestVectors.get_RPIK_HKDF_INFO_STRING(),
                        ContactTracingFeature.rollingProximityIdKeySizeBytes());
        assertArrayEquals(rpiKey, TestVectors.get_RPIK());
    }

    @Test
    public void getCachedData_sameEnIntervalNumber_sameAsDirectlyGenerated() {
        int idsPerKey =
                ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
        int baseEnIntervalNumber =
                TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY
                        - (ContactTracingFeature.contactRecordDaysToKeep() * idsPerKey);
        int cacheSize = ContactTracingFeature.contactRecordDaysToKeep() + 1;
        byte[] aesPadding = ContactTracingFeature.rpidAesPaddedString().getBytes(UTF_8);

        PaddedDataCache paddedDataCache =
                PaddedDataCache.createInstance(baseEnIntervalNumber, cacheSize, aesPadding, idsPerKey);

        for (int i = 0, testEnIntervalNumber = baseEnIntervalNumber;
             i < cacheSize;
             i++, testEnIntervalNumber += idsPerKey) {
            assertArrayEquals(paddedDataCache.getCachedData(testEnIntervalNumber),
                    RollingProximityIdGeneratorBase.generateAggregatePaddedData(
                            testEnIntervalNumber, idsPerKey, aesPadding));
        }
    }

    @Test
    public void getCachedData_notAlignedEnIntervalNumber_mustReturnNull() {
        int idsPerKey =
                ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
        int baseEnIntervalNumber =
                TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY
                        - (ContactTracingFeature.contactRecordDaysToKeep() * idsPerKey);
        int cacheSize = ContactTracingFeature.contactRecordDaysToKeep() + 1;
        byte[] aesPadding = ContactTracingFeature.rpidAesPaddedString().getBytes(UTF_8);

        PaddedDataCache paddedDataCache =
                PaddedDataCache.createInstance(baseEnIntervalNumber, cacheSize, aesPadding, idsPerKey);

        assertEquals(paddedDataCache.getCachedData(baseEnIntervalNumber + 1), null);
        assertEquals(paddedDataCache.getCachedData(
                TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY - 1), null);
    }

    @Test
    public void getCachedData_outOfRangeEnIntervalNumber_mustReturnNull() {
        int idsPerKey =
                ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
        int baseEnIntervalNumber =
                TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY
                        - (ContactTracingFeature.contactRecordDaysToKeep() * idsPerKey);
        int cacheSize = ContactTracingFeature.contactRecordDaysToKeep() + 1;
        byte[] aesPadding = ContactTracingFeature.rpidAesPaddedString().getBytes(UTF_8);

        PaddedDataCache paddedDataCache =
                PaddedDataCache.createInstance(baseEnIntervalNumber, cacheSize, aesPadding, idsPerKey);

        assertEquals(paddedDataCache.getCachedData(baseEnIntervalNumber - idsPerKey), null);
        assertEquals(
                paddedDataCache.getCachedData(
                        TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY + idsPerKey), null);
    }

    @Test
    public void generateIds_numIdsTooLarge_shouldNotCrash() throws Exception {
        TemporaryExposureKey temporaryExposureKey =
                new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                        .setRollingStartIntervalNumber(0)
                        .setRollingPeriod(145)
                        .setKeyData(
                                TemporaryExposureKeySupport.getMinKey(DayNumber.MIN_DAY_NUMBER).getKeyData())
                        .setTransmissionRiskLevel(1)
                        .build();

        List<GeneratedRollingProximityId> generatedIds =
                new RollingProximityIdGenerator.Factory()
                        .getInstance(
                                AesEcbEncryptor.create(),
                                temporaryExposureKey.getKeyData(),
                                temporaryExposureKey.getRollingStartIntervalNumber(),
                                TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey),
                                ContactTracingFeature.rollingProximityIdKeySizeBytes(),
                                ContactTracingFeature.rpikHkdfInfoString(),
                                ContactTracingFeature.rpidAesPaddedString())
                        .generateIds(
                                new byte[145 * ContactTracingFeature.contactIdLength()]);
        assertEquals(generatedIds.size(), 145);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.testing;

import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
import com.google.samples.exposurenotification.data.BluetoothMetadata;
import com.google.samples.exposurenotification.data.RollingProximityId;
import com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataGenerator;
import com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataHelper;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import org.junit.Test;
import org.junit.runner.RunWith;

import static com.google.common.io.BaseEncoding.base16;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

@RunWith(AndroidJUnit4.class)
public class AssociatedEncryptedMetadataGeneratorTest {

    @Test
    public void decryptEncryptedBleMetadata_mustGetOriginalValue() throws Exception {
        RollingProximityId rollingProximityId =
                new RollingProximityId(base16().decode("AABBCCDDEEFF00112233445566778899"));
        TemporaryExposureKey temporaryExposureKey =
                new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                        .setKeyData(base16().decode("FEDCBA9876543210FEDCBA9876543210"))
                        .setRollingStartIntervalNumber(0)
                        .build();
        BluetoothMetadata metadata = BluetoothMetadata.fromBytes(new byte[]{0x01, 0x02, 0x03, 0x04});

        AssociatedEncryptedMetadataGenerator generator =
                new AssociatedEncryptedMetadataGenerator(temporaryExposureKey);
        AssociatedEncryptedMetadata encryptedMetadata =
                generator.encrypt(rollingProximityId, metadata);

        assertEquals(generator.decrypt(rollingProximityId, encryptedMetadata), metadata);
    }

    @Test
    public void encrypt_usingTestVector() throws Exception {
        byte[] aemKey = TestVectors.get_AEMK();
        byte[] bleMetadata = TestVectors.get_BLE_METADATA();

        for (TestVectors.AdvertisedData element : TestVectors.ADVERTISED_DATA) {
            byte[] encryptedMetadata =
                    AssociatedEncryptedMetadataHelper.encryptOrDecrypt(
                            aemKey, element.get_RPI(), bleMetadata);
            assertArrayEquals(encryptedMetadata, element.get_AEM());
        }
    }

    @Test
    public void decrypt_usingTestVector() throws Exception {
        byte[] aemKey = TestVectors.get_AEMK();
        byte[] bleMetadata = TestVectors.get_BLE_METADATA();

        for (TestVectors.AdvertisedData element : TestVectors.ADVERTISED_DATA) {
            byte[] decryptedMetadata =
                    AssociatedEncryptedMetadataHelper.encryptOrDecrypt(
                            aemKey, element.get_RPI(), element.get_AEM());
            assertArrayEquals(decryptedMetadata, bleMetadata);
        }
    }

    // Runs the AEMK test vector on the spec:
    @Test
    public void generateAemKey_usingTestVector() throws Exception {
        byte[] aemKey =
                AssociatedEncryptedMetadataHelper.generateAemKey(
                        TestVectors.get_TEMPORARY_TRACING_KEY(),
                        ContactTracingFeature.aemkHkdfInfoString(),
                        ContactTracingFeature.associatedMetadataEncryptionKeySizeBytes());
        assertArrayEquals(aemKey, TestVectors.get_AEMK());
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.testing;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Range;

import org.junit.Assert;

/**
 * Java version of Exposure Notification test vectors (v1.2).
 *
 * <p>{@link #get_TEMPORARY_TRACING_KEY} is a random key generated at {@link
 * #KEY_GENERATION_NSECONDS}.
 *
 * <p>Derived from those values:
 *
 * <ul>
 *   <li>{@link #CTINTERVAL_NUMBER_OF_GENERATED_KEY} is derived from the generation time.
 *   <li>{@link #get_RPIK} returns the expected Rolling Proximity Indicator Key
 *   <li>{@link #get_AEMK} returns the expected Associated Metadata Encryption Key
 * </ul>
 *
 * <p>Given the associated metadata in {@link #get_BLE_METADATA}, the expected values at each 10
 * minute interval are provided in {@link #ADVERTISED_DATA}, e.g.
 *
 * <pre>{@code
 * for (int i = 0; i < 144; i++) {
 *   byte[] rpi = generateRpi(i, TestVectors.get_RPIK());
 *   byte[] aem = generateAem(i, TestVectors.get_AEMK(), testVectors,get_BLE_METADATA());
 *   assertThat(rpi).isEqualTo(ADVERTISED_DATA[i].get_RPI());
 *   assertThat(aem).isEqualTo(ADVERTISED_DATA[i].get_AEM());
 * }
 * }</pre>
 *
 * <p>All of the byte array "constants" are accessed through static functions which return a fresh
 * copy of the master array, to avoid any possibility of corruption by the tests.
 *
 * <p>The original C++ header is in TestVectors.h.txt.
 */
public class TestVectors {

    public static byte[] asBytes(int... ints) {
        byte[] bytes = new byte[ints.length];
        for (int i = 0; i < ints.length; i++) {
            int value = ints[i];
            Assert.assertTrue(Range.closed(0, 255).contains(value));
            bytes[i] = (byte) value;
        }
        return bytes;
    }

    /**
     * Class holding a matched pair of RPI and AEM values.
     */
    public static class AdvertisedData {
        private final byte[] rollingProximityIndicator;
        private final byte[] associatedEncryptedMetadata;

        AdvertisedData(byte[] rollingProximityIndicator, byte[] associatedEncryptedMetadata) {
            this.rollingProximityIndicator = rollingProximityIndicator;
            this.associatedEncryptedMetadata = associatedEncryptedMetadata;
        }

        public byte[] get_RPI() {
            return rollingProximityIndicator.clone();
        }

        public byte[] get_AEM() {
            return associatedEncryptedMetadata.clone();
        }
    }

    public static byte[] get_TEMPORARY_TRACING_KEY() {
        return TEMPORARY_TRACING_KEY.clone();
    }

    public static byte[] get_RPIK() {
        return RPIK.clone();
    }

    public static byte[] get_AEMK() {
        return AEMK.clone();
    }

    public static byte[] get_BLE_METADATA() {
        return BLE_METADATA.clone();
    }

    public static String get_RPIK_HKDF_INFO_STRING() {
        return "EN-RPIK";
    }

    public static String get_RPI_AES_PADDED_STRING() {
        return "EN-RPI";
    }

    public static String get_AEMK_HKDF_INFO_STRING() {
        return "EN-AEMK";
    }

    // From TestVectors.h.txt
    // ------------------------------------------------------------------------------
    public static final int KEY_GENERATION_NSECONDS = 1585785600;
    public static final int CTINTERVAL_NUMBER_OF_GENERATED_KEY = 2642976;
    public static final int ID_ROLLING_PERIOD_MINUTES = 10;
    public static final int KEY_ROLLING_PERIOD_MULTIPLE_OF_ID_PERIOD = 144;

    private static final byte[] TEMPORARY_TRACING_KEY =
            asBytes(
                    0x75, 0xc7, 0x34, 0xc6, 0xdd, 0x1a, 0x78, 0x2d, 0xe7, 0xa9, 0x65, 0xda, 0x5e, 0xb9, 0x31,
                    0x25);

    private static final byte[] RPIK =
            asBytes(
                    0x18, 0x5a, 0xd9, 0x1d, 0xb6, 0x9e, 0xc7, 0xdd, 0x04, 0x89, 0x60, 0xf1, 0xf3, 0xba, 0x61,
                    0x75);
    private static final byte[] AEMK =
            asBytes(
                    0xd5, 0x7c, 0x46, 0xaf, 0x7a, 0x1d, 0x83, 0x96, 0x5b, 0x9b, 0xed, 0x8b, 0xd1, 0x52, 0x93,
                    0x6a);
    private static final byte[] BLE_METADATA = asBytes(0x40, 0x08, 0x00, 0x00);
    private static final byte[] RPI0 =
            asBytes(
                    0x8b, 0xe6, 0xcd, 0x37, 0x1c, 0x5c, 0x89, 0x16, 0x04, 0xbf, 0xbe, 0x49, 0xdf, 0x84, 0x50,
                    0x96);
    private static final byte[] AEM0 = asBytes(0x72, 0x03, 0x38, 0x74);
    private static final byte[] RPI1 =
            asBytes(
                    0x3c, 0x9a, 0x1d, 0xe5, 0xdd, 0x6b, 0x02, 0xaf, 0xa7, 0xfd, 0xed, 0x7b, 0x57, 0x0b, 0x3e,
                    0x56);
    private static final byte[] AEM1 = asBytes(0xc2, 0x92, 0x11, 0xb1);
    private static final byte[] RPI2 =
            asBytes(
                    0x24, 0x3f, 0xfe, 0x9a, 0x3b, 0x08, 0xbd, 0xed, 0x30, 0x94, 0xba, 0xc8, 0x63, 0x0b, 0xb8,
                    0xad);
    private static final byte[] AEM2 = asBytes(0x6a, 0xdf, 0xad, 0x03);
    private static final byte[] RPI3 =
            asBytes(
                    0xdf, 0xc3, 0xed, 0x26, 0x5e, 0x97, 0xd0, 0xea, 0xbb, 0x63, 0x0e, 0x16, 0x8b, 0x42, 0x14,
                    0xed);
    private static final byte[] AEM3 = asBytes(0xf1, 0xe2, 0xf8, 0x0b);
    private static final byte[] RPI4 =
            asBytes(
                    0xb3, 0xb8, 0x5a, 0x69, 0xeb, 0xae, 0xc7, 0x8d, 0xb7, 0x39, 0x85, 0x2d, 0x1f, 0x34, 0xe0,
                    0xfa);
    private static final byte[] AEM4 = asBytes(0x56, 0x68, 0xc4, 0x74);
    private static final byte[] RPI5 =
            asBytes(
                    0x29, 0x8a, 0xbd, 0x6f, 0xda, 0xd2, 0x9e, 0xfb, 0xf0, 0xf8, 0x5a, 0x63, 0x95, 0x6c, 0xf1,
                    0x88);
    private static final byte[] AEM5 = asBytes(0xf7, 0xb9, 0x7e, 0x84);
    private static final byte[] RPI6 =
            asBytes(
                    0x10, 0x5e, 0x82, 0x21, 0xdd, 0x60, 0x3d, 0x25, 0xb9, 0x4a, 0xba, 0x0c, 0x3c, 0xc8, 0xde,
                    0xe1);
    private static final byte[] AEM6 = asBytes(0xee, 0xab, 0xfd, 0xc7);
    private static final byte[] RPI7 =
            asBytes(
                    0x98, 0x88, 0xca, 0x7e, 0x67, 0x38, 0xec, 0x4b, 0xc6, 0xe4, 0x20, 0xb2, 0x0f, 0x87, 0x8b,
                    0x3a);
    private static final byte[] AEM7 = asBytes(0x9f, 0x84, 0xa9, 0xa6);
    private static final byte[] RPI8 =
            asBytes(
                    0x86, 0x94, 0xa7, 0x9a, 0xe9, 0x96, 0x71, 0xbe, 0x3f, 0x18, 0xaa, 0xf6, 0xb0, 0x90, 0x65,
                    0x7a);
    private static final byte[] AEM8 = asBytes(0xc1, 0x1a, 0xcb, 0x4d);
    private static final byte[] RPI9 =
            asBytes(
                    0xb8, 0xcd, 0xe2, 0x8a, 0xd2, 0x0d, 0x1c, 0xd2, 0xfd, 0xd7, 0x36, 0x9d, 0xc0, 0xc6, 0xf7,
                    0xc8);
    private static final byte[] AEM9 = asBytes(0x1f, 0x8d, 0x53, 0xde);
    private static final byte[] RPI10 =
            asBytes(
                    0xf6, 0x01, 0x23, 0xc6, 0x7b, 0xd2, 0xfc, 0x4b, 0x62, 0x00, 0x2b, 0x4b, 0x7d, 0x59, 0x5b,
                    0xa6);
    private static final byte[] AEM10 = asBytes(0xe0, 0xa7, 0xf9, 0xf2);
    private static final byte[] RPI11 =
            asBytes(
                    0x8a, 0x84, 0xc8, 0x6e, 0x05, 0xf7, 0xa8, 0x77, 0x2a, 0xae, 0x7a, 0x80, 0x68, 0x6e, 0x1a,
                    0x1c);
    private static final byte[] AEM11 = asBytes(0xf4, 0xb7, 0xb0, 0x9d);
    private static final byte[] RPI12 =
            asBytes(
                    0xac, 0x46, 0x1b, 0xf2, 0xb9, 0x3b, 0x0d, 0x90, 0x84, 0x17, 0x46, 0xf5, 0x1a, 0xde, 0xd6,
                    0xc0);
    private static final byte[] AEM12 = asBytes(0x73, 0x91, 0xdd, 0x64);
    private static final byte[] RPI13 =
            asBytes(
                    0xf2, 0x56, 0x7e, 0xed, 0x0a, 0xa1, 0xcd, 0xf2, 0xcd, 0x3d, 0x0b, 0xd2, 0x82, 0x52, 0xf1,
                    0x96);
    private static final byte[] AEM13 = asBytes(0x12, 0x7d, 0xb0, 0x9e);
    private static final byte[] RPI14 =
            asBytes(
                    0xa2, 0x55, 0x22, 0x5b, 0xaa, 0x9e, 0x37, 0xb7, 0x30, 0xa9, 0x5f, 0x99, 0x7a, 0x69, 0x72,
                    0xf5);
    private static final byte[] AEM14 = asBytes(0x43, 0x5e, 0xa5, 0x56);
    private static final byte[] RPI15 =
            asBytes(
                    0x04, 0xcf, 0xae, 0xe4, 0x10, 0x21, 0xcb, 0x7d, 0x4d, 0x02, 0x0b, 0x30, 0x6b, 0x24, 0xbe,
                    0xa8);
    private static final byte[] AEM15 = asBytes(0xdc, 0x1f, 0x19, 0x06);
    private static final byte[] RPI16 =
            asBytes(
                    0xf5, 0x37, 0x2b, 0xbc, 0x92, 0xf7, 0x80, 0x59, 0x64, 0x70, 0x1e, 0x87, 0x9b, 0x48, 0xd4,
                    0x31);
    private static final byte[] AEM16 = asBytes(0xb3, 0xea, 0xaf, 0xdd);
    private static final byte[] RPI17 =
            asBytes(
                    0xf2, 0xc8, 0x11, 0x54, 0x63, 0x82, 0xb1, 0x0d, 0xf1, 0xac, 0x06, 0xc3, 0x2c, 0x61, 0x7b,
                    0xa7);
    private static final byte[] AEM17 = asBytes(0xcd, 0x51, 0x86, 0xf9);
    private static final byte[] RPI18 =
            asBytes(
                    0xf8, 0xf3, 0x44, 0x1f, 0x76, 0x22, 0x3d, 0xac, 0x15, 0xec, 0x6b, 0x35, 0xfd, 0xb2, 0x51,
                    0x40);
    private static final byte[] AEM18 = asBytes(0x7f, 0x38, 0x7e, 0x7f);
    private static final byte[] RPI19 =
            asBytes(
                    0xd0, 0x71, 0x83, 0xdb, 0x3c, 0x80, 0x45, 0x08, 0x7d, 0x61, 0xee, 0x9e, 0x73, 0x0c, 0x93,
                    0x06);
    private static final byte[] AEM19 = asBytes(0x1a, 0x42, 0xa1, 0x5b);
    private static final byte[] RPI20 =
            asBytes(
                    0xc7, 0x42, 0xdd, 0x9c, 0x96, 0xa3, 0xe6, 0xfa, 0x7c, 0x4f, 0x22, 0x62, 0x1d, 0xac, 0xc2,
                    0x4d);
    private static final byte[] AEM20 = asBytes(0x06, 0x39, 0x0d, 0xed);
    private static final byte[] RPI21 =
            asBytes(
                    0x08, 0x23, 0x33, 0xfa, 0xd9, 0xfa, 0x29, 0x2a, 0xb8, 0x99, 0xd6, 0x00, 0x0c, 0x65, 0x97,
                    0x97);
    private static final byte[] AEM21 = asBytes(0x0f, 0x01, 0xbc, 0xd7);
    private static final byte[] RPI22 =
            asBytes(
                    0xbe, 0x43, 0x00, 0xda, 0xfb, 0x8d, 0x07, 0xc8, 0x8c, 0xb2, 0xb5, 0x07, 0x7a, 0x06, 0x11,
                    0x66);
    private static final byte[] AEM22 = asBytes(0xc7, 0xbf, 0xbb, 0x92);
    private static final byte[] RPI23 =
            asBytes(
                    0x5f, 0xed, 0x1b, 0x4d, 0x3b, 0x3a, 0x13, 0x33, 0x2f, 0x05, 0x44, 0x75, 0x60, 0x35, 0x26,
                    0x32);
    private static final byte[] AEM23 = asBytes(0x21, 0x7c, 0x8e, 0x4a);
    private static final byte[] RPI24 =
            asBytes(
                    0xfd, 0x1e, 0xe2, 0xcc, 0x5c, 0x60, 0xe6, 0xee, 0xe6, 0x1f, 0x04, 0x91, 0x9f, 0x67, 0x59,
                    0xa7);
    private static final byte[] AEM24 = asBytes(0xee, 0x63, 0x9b, 0xd6);
    private static final byte[] RPI25 =
            asBytes(
                    0x96, 0xad, 0xf5, 0xb8, 0xdc, 0x7e, 0xe7, 0x5d, 0xf4, 0x6f, 0xbd, 0x8a, 0x1f, 0xc4, 0xad,
                    0x0d);
    private static final byte[] AEM25 = asBytes(0x60, 0x8c, 0x13, 0x0f);
    private static final byte[] RPI26 =
            asBytes(
                    0xfa, 0x4b, 0xa2, 0x20, 0x6d, 0x42, 0xa1, 0xc8, 0x0d, 0x52, 0x48, 0xae, 0x68, 0x83, 0x09,
                    0xa4);
    private static final byte[] AEM26 = asBytes(0x74, 0xb2, 0xc8, 0x73);
    private static final byte[] RPI27 =
            asBytes(
                    0xa5, 0x90, 0xf9, 0x5d, 0xbf, 0x24, 0x02, 0x61, 0xda, 0x10, 0x1a, 0x7c, 0xdb, 0x24, 0xdb,
                    0xba);
    private static final byte[] AEM27 = asBytes(0x95, 0x6a, 0x95, 0xeb);
    private static final byte[] RPI28 =
            asBytes(
                    0x67, 0xe8, 0x1b, 0x91, 0xd1, 0xcf, 0x9e, 0x09, 0x58, 0x13, 0x54, 0x29, 0xda, 0xd0, 0x1e,
                    0x82);
    private static final byte[] AEM28 = asBytes(0xc6, 0xfa, 0x3c, 0x7a);
    private static final byte[] RPI29 =
            asBytes(
                    0x18, 0x3c, 0xac, 0x22, 0x36, 0xc3, 0xe0, 0x53, 0x3b, 0xe4, 0x70, 0x4d, 0x83, 0x6e, 0x47,
                    0x55);
    private static final byte[] AEM29 = asBytes(0xa2, 0xab, 0xc7, 0x03);
    private static final byte[] RPI30 =
            asBytes(
                    0x34, 0x11, 0x62, 0x55, 0x1c, 0x29, 0x31, 0x9b, 0xc5, 0x35, 0x38, 0xed, 0xfc, 0xf2, 0x30,
                    0x40);
    private static final byte[] AEM30 = asBytes(0x80, 0x09, 0xb2, 0xaa);
    private static final byte[] RPI31 =
            asBytes(
                    0x22, 0x51, 0x68, 0x70, 0x18, 0x21, 0x4b, 0x65, 0xdd, 0x8e, 0xe8, 0x3e, 0xae, 0xd3, 0x30,
                    0xab);
    private static final byte[] AEM31 = asBytes(0xcf, 0xe8, 0x04, 0xcb);
    private static final byte[] RPI32 =
            asBytes(
                    0xa4, 0xcf, 0x6e, 0x50, 0x21, 0x5f, 0xe2, 0x78, 0xcc, 0x5c, 0xff, 0x1b, 0x05, 0x34, 0xa3,
                    0xe0);
    private static final byte[] AEM32 = asBytes(0xcb, 0x2a, 0x7e, 0x22);
    private static final byte[] RPI33 =
            asBytes(
                    0xdf, 0x8f, 0x2a, 0xc3, 0x03, 0x23, 0x2b, 0x2e, 0x5b, 0x3e, 0xfd, 0x86, 0x81, 0xaa, 0xa8,
                    0xdd);
    private static final byte[] AEM33 = asBytes(0x65, 0x03, 0xa7, 0x27);
    private static final byte[] RPI34 =
            asBytes(
                    0xba, 0x2e, 0x75, 0xd7, 0xf4, 0x8c, 0xf5, 0x5c, 0x0c, 0x86, 0x8f, 0xd4, 0x5c, 0xf1, 0x6b,
                    0x5c);
    private static final byte[] AEM34 = asBytes(0x8f, 0x29, 0x78, 0x7e);
    private static final byte[] RPI35 =
            asBytes(
                    0xec, 0x6a, 0x40, 0x05, 0x8d, 0xeb, 0xff, 0xff, 0x3c, 0x51, 0x97, 0x7f, 0x24, 0x56, 0x2e,
                    0x21);
    private static final byte[] AEM35 = asBytes(0xb2, 0xad, 0xd3, 0xb7);
    private static final byte[] RPI36 =
            asBytes(
                    0x6a, 0x68, 0xe3, 0x0b, 0x2f, 0xb9, 0x3b, 0x5d, 0xf7, 0x8e, 0xe3, 0xa9, 0xa3, 0x50, 0xa6,
                    0xce);
    private static final byte[] AEM36 = asBytes(0x1b, 0xfb, 0x46, 0x0a);
    private static final byte[] RPI37 =
            asBytes(
                    0x8d, 0x33, 0xa7, 0x05, 0x62, 0x62, 0x99, 0x94, 0xf8, 0xdf, 0x99, 0x05, 0x2b, 0x0e, 0xb6,
                    0x9a);
    private static final byte[] AEM37 = asBytes(0x8e, 0xcf, 0x07, 0x7b);
    private static final byte[] RPI38 =
            asBytes(
                    0x21, 0x05, 0x3c, 0xcb, 0x8f, 0x92, 0x51, 0x11, 0xe2, 0x54, 0xbd, 0x69, 0x4e, 0x97, 0x94,
                    0x6b);
    private static final byte[] AEM38 = asBytes(0x2b, 0xa7, 0x7b, 0x38);
    private static final byte[] RPI39 =
            asBytes(
                    0xe1, 0xc9, 0xcd, 0xf2, 0x0f, 0x90, 0x0a, 0xe6, 0xd2, 0x4b, 0xf7, 0xbc, 0xb4, 0xe6, 0x61,
                    0x35);
    private static final byte[] AEM39 = asBytes(0x8c, 0x12, 0x40, 0xde);
    private static final byte[] RPI40 =
            asBytes(
                    0xba, 0xf7, 0x89, 0xf5, 0x50, 0x94, 0x6b, 0x43, 0x10, 0x64, 0x45, 0x07, 0x71, 0xb2, 0xa1,
                    0x43);
    private static final byte[] AEM40 = asBytes(0x43, 0x89, 0x99, 0xb6);
    private static final byte[] RPI41 =
            asBytes(
                    0xf7, 0xf9, 0x1e, 0xc2, 0x50, 0x85, 0xd0, 0x35, 0x3e, 0x02, 0x78, 0xe5, 0x98, 0xcc, 0x62,
                    0x01);
    private static final byte[] AEM41 = asBytes(0x1d, 0xcb, 0xdc, 0x6e);
    private static final byte[] RPI42 =
            asBytes(
                    0xc8, 0x93, 0x3d, 0x70, 0x22, 0x0b, 0xa9, 0xc8, 0xc1, 0x48, 0x39, 0x3a, 0x39, 0x59, 0xd2,
                    0x56);
    private static final byte[] AEM42 = asBytes(0x63, 0x85, 0xff, 0xda);
    private static final byte[] RPI43 =
            asBytes(
                    0xf2, 0x93, 0x2e, 0x6e, 0x6e, 0xf6, 0x0f, 0x0f, 0x5b, 0xbc, 0xe4, 0x39, 0x10, 0x0a, 0x90,
                    0xe4);
    private static final byte[] AEM43 = asBytes(0x69, 0x6c, 0x0c, 0xdf);
    private static final byte[] RPI44 =
            asBytes(
                    0x18, 0xf5, 0xd8, 0x10, 0x2a, 0x59, 0x30, 0xd8, 0x02, 0x30, 0xf2, 0xc3, 0x9a, 0x42, 0x66,
                    0xd6);
    private static final byte[] AEM44 = asBytes(0x63, 0x84, 0x2b, 0xba);
    private static final byte[] RPI45 =
            asBytes(
                    0xe6, 0x07, 0x5c, 0x28, 0x93, 0x9f, 0xb0, 0xc6, 0x72, 0x46, 0xce, 0x38, 0xc5, 0xff, 0x93,
                    0x8a);
    private static final byte[] AEM45 = asBytes(0xb7, 0xe2, 0x2c, 0x60);
    private static final byte[] RPI46 =
            asBytes(
                    0x66, 0x16, 0x88, 0x62, 0xbc, 0x44, 0x5f, 0x48, 0xe5, 0xb0, 0xed, 0x07, 0xe1, 0xdf, 0x3f,
                    0x5a);
    private static final byte[] AEM46 = asBytes(0xd2, 0xe7, 0xd6, 0xd8);
    private static final byte[] RPI47 =
            asBytes(
                    0x1d, 0x0a, 0x01, 0xc3, 0x8d, 0xa4, 0xac, 0x41, 0xec, 0x7a, 0x63, 0x8f, 0x5d, 0xf7, 0x05,
                    0xa9);
    private static final byte[] AEM47 = asBytes(0x91, 0xbe, 0x92, 0x2c);
    private static final byte[] RPI48 =
            asBytes(
                    0xa7, 0x37, 0x00, 0x1a, 0x2d, 0x2f, 0x80, 0x2c, 0x64, 0x78, 0x9a, 0x99, 0x52, 0xe6, 0xd1,
                    0xa7);
    private static final byte[] AEM48 = asBytes(0x7e, 0x04, 0x21, 0xbb);
    private static final byte[] RPI49 =
            asBytes(
                    0x7c, 0x37, 0x25, 0xb6, 0x08, 0x4e, 0x68, 0x1f, 0xb3, 0x4d, 0x26, 0xc3, 0xa3, 0x94, 0xa6,
                    0x43);
    private static final byte[] AEM49 = asBytes(0x7e, 0xa5, 0x20, 0x9f);
    private static final byte[] RPI50 =
            asBytes(
                    0x26, 0xd1, 0xf8, 0x36, 0x55, 0x7a, 0x25, 0x9a, 0x81, 0xb5, 0xdb, 0x54, 0x19, 0xc6, 0xa7,
                    0x29);
    private static final byte[] AEM50 = asBytes(0x1c, 0x92, 0x06, 0x28);
    private static final byte[] RPI51 =
            asBytes(
                    0xeb, 0xc2, 0xa6, 0x06, 0x28, 0x54, 0xd1, 0xec, 0x62, 0x7b, 0x1f, 0x6e, 0x84, 0x32, 0xe1,
                    0x66);
    private static final byte[] AEM51 = asBytes(0x4a, 0x76, 0x46, 0x32);
    private static final byte[] RPI52 =
            asBytes(
                    0x11, 0x32, 0x74, 0xe8, 0x0c, 0x31, 0xcf, 0xcd, 0x81, 0xc2, 0xad, 0x08, 0x64, 0x44, 0x51,
                    0x78);
    private static final byte[] AEM52 = asBytes(0x69, 0xa7, 0x49, 0xb6);
    private static final byte[] RPI53 =
            asBytes(
                    0x45, 0x67, 0x97, 0x6c, 0x48, 0xbe, 0x72, 0x59, 0x06, 0x24, 0x7d, 0x0b, 0xd8, 0x1b, 0xb8,
                    0x11);
    private static final byte[] AEM53 = asBytes(0x6f, 0x94, 0x85, 0xdb);
    private static final byte[] RPI54 =
            asBytes(
                    0x74, 0x81, 0x54, 0xba, 0x52, 0x3a, 0x1a, 0xa8, 0x10, 0xb7, 0x06, 0x2a, 0x13, 0xe5, 0xaa,
                    0x68);
    private static final byte[] AEM54 = asBytes(0x74, 0x78, 0x07, 0x23);
    private static final byte[] RPI55 =
            asBytes(
                    0x30, 0xbc, 0xeb, 0x33, 0x45, 0x74, 0x51, 0x53, 0x35, 0x23, 0x65, 0x99, 0x85, 0x87, 0xcd,
                    0x10);
    private static final byte[] AEM55 = asBytes(0x89, 0x17, 0xda, 0x61);
    private static final byte[] RPI56 =
            asBytes(
                    0x8f, 0x2d, 0x7b, 0x87, 0x00, 0xa8, 0x2f, 0xd4, 0x51, 0x4d, 0xfa, 0x42, 0x02, 0xee, 0x29,
                    0x8f);
    private static final byte[] AEM56 = asBytes(0xfe, 0xc4, 0xf8, 0xb1);
    private static final byte[] RPI57 =
            asBytes(
                    0x0e, 0x66, 0x49, 0x53, 0x70, 0x0c, 0xdf, 0xc0, 0xd2, 0x79, 0x2f, 0xad, 0xf0, 0x73, 0x29,
                    0xeb);
    private static final byte[] AEM57 = asBytes(0x78, 0x1a, 0x3e, 0xaf);
    private static final byte[] RPI58 =
            asBytes(
                    0xf4, 0x49, 0x58, 0xc4, 0xdd, 0x70, 0xd9, 0x96, 0x8a, 0x26, 0xfd, 0x60, 0xba, 0x92, 0x72,
                    0x90);
    private static final byte[] AEM58 = asBytes(0xb9, 0x75, 0x9b, 0x61);
    private static final byte[] RPI59 =
            asBytes(
                    0x55, 0xfd, 0x2f, 0x6c, 0xbd, 0xe0, 0xe1, 0x3f, 0xd2, 0x2c, 0x0b, 0x3d, 0xb1, 0x62, 0x28,
                    0xe5);
    private static final byte[] AEM59 = asBytes(0x48, 0x3c, 0x94, 0x10);
    private static final byte[] RPI60 =
            asBytes(
                    0x49, 0xf3, 0xf9, 0xd1, 0x24, 0x69, 0xdc, 0xc9, 0xed, 0x35, 0x63, 0x64, 0xc3, 0x00, 0x66,
                    0xe4);
    private static final byte[] AEM60 = asBytes(0x2d, 0xa6, 0xeb, 0xac);
    private static final byte[] RPI61 =
            asBytes(
                    0xc5, 0x7d, 0x2d, 0x6e, 0x0d, 0x25, 0xa0, 0x65, 0x1c, 0xd7, 0x27, 0x86, 0xf8, 0xc9, 0x51,
                    0xce);
    private static final byte[] AEM61 = asBytes(0x43, 0xea, 0xcf, 0x34);
    private static final byte[] RPI62 =
            asBytes(
                    0x88, 0xef, 0x25, 0x63, 0x51, 0xac, 0x49, 0xdf, 0xd1, 0x5a, 0xb5, 0xa2, 0xde, 0x97, 0xc0,
                    0x13);
    private static final byte[] AEM62 = asBytes(0xb9, 0xa1, 0x36, 0x53);
    private static final byte[] RPI63 =
            asBytes(
                    0xd0, 0xfb, 0x6f, 0xd6, 0xdb, 0x89, 0xda, 0x52, 0x36, 0x1f, 0x1a, 0x30, 0xfb, 0x43, 0x6c,
                    0xe7);
    private static final byte[] AEM63 = asBytes(0x35, 0x6d, 0xea, 0x55);
    private static final byte[] RPI64 =
            asBytes(
                    0x8a, 0x42, 0xa3, 0x30, 0xf0, 0x19, 0x28, 0xe5, 0x16, 0x31, 0x23, 0x19, 0x81, 0x60, 0x3f,
                    0xd5);
    private static final byte[] AEM64 = asBytes(0x35, 0x05, 0xb7, 0xf3);
    private static final byte[] RPI65 =
            asBytes(
                    0x6e, 0x51, 0xb2, 0xa2, 0xae, 0xcb, 0xab, 0x1d, 0xf8, 0x08, 0x26, 0xef, 0x6d, 0x1e, 0x19,
                    0x58);
    private static final byte[] AEM65 = asBytes(0x5c, 0x05, 0xcd, 0x94);
    private static final byte[] RPI66 =
            asBytes(
                    0x3c, 0xe6, 0x81, 0xa2, 0x8b, 0x1b, 0xe1, 0x9c, 0x9e, 0x36, 0xb9, 0xc5, 0x80, 0xb1, 0x23,
                    0xab);
    private static final byte[] AEM66 = asBytes(0x98, 0x47, 0x45, 0xd6);
    private static final byte[] RPI67 =
            asBytes(
                    0x1e, 0x95, 0x8e, 0xd8, 0x9b, 0x86, 0xb9, 0x89, 0x77, 0xf7, 0x9e, 0x1b, 0x83, 0xf3, 0xd0,
                    0x5f);
    private static final byte[] AEM67 = asBytes(0x93, 0xfd, 0xf7, 0x08);
    private static final byte[] RPI68 =
            asBytes(
                    0x1d, 0x66, 0x7b, 0x01, 0xd6, 0x63, 0x4b, 0x5f, 0x3b, 0x6f, 0x33, 0xac, 0x4b, 0x15, 0x0d,
                    0x23);
    private static final byte[] AEM68 = asBytes(0xd7, 0x0f, 0x74, 0x4c);
    private static final byte[] RPI69 =
            asBytes(
                    0x67, 0xf2, 0x22, 0x15, 0x6c, 0x51, 0x7d, 0xeb, 0xc0, 0x70, 0x68, 0xcb, 0xc5, 0xee, 0xc1,
                    0xdd);
    private static final byte[] AEM69 = asBytes(0x60, 0x57, 0x8c, 0x31);
    private static final byte[] RPI70 =
            asBytes(
                    0xa8, 0x45, 0x4b, 0x9c, 0x94, 0x7d, 0x16, 0x25, 0xee, 0x3f, 0xba, 0x26, 0x07, 0xc2, 0x3a,
                    0xff);
    private static final byte[] AEM70 = asBytes(0x79, 0x00, 0xee, 0xad);
    private static final byte[] RPI71 =
            asBytes(
                    0x41, 0x6f, 0xfc, 0x7a, 0x32, 0xfc, 0xfd, 0xa9, 0xa3, 0x16, 0xd0, 0x17, 0x90, 0xe3, 0x19,
                    0x45);
    private static final byte[] AEM71 = asBytes(0xc1, 0x22, 0xad, 0x68);
    private static final byte[] RPI72 =
            asBytes(
                    0xc1, 0x9a, 0x30, 0xc3, 0x9c, 0x9c, 0x3a, 0x08, 0x9b, 0xca, 0xdd, 0xe1, 0xc6, 0x69, 0x94,
                    0x47);
    private static final byte[] AEM72 = asBytes(0x34, 0x0a, 0xa3, 0x82);
    private static final byte[] RPI73 =
            asBytes(
                    0x78, 0x6d, 0xdf, 0xae, 0x6f, 0xc7, 0x7c, 0x4c, 0x41, 0x0c, 0x4e, 0xc3, 0x2d, 0x34, 0x24,
                    0x7d);
    private static final byte[] AEM73 = asBytes(0x67, 0xc6, 0x0d, 0xfb);
    private static final byte[] RPI74 =
            asBytes(
                    0xef, 0x0f, 0xd3, 0xa9, 0x5b, 0x96, 0x61, 0xe1, 0xfc, 0xcb, 0x4e, 0x30, 0xcd, 0xe3, 0x2c,
                    0x51);
    private static final byte[] AEM74 = asBytes(0x45, 0x56, 0xb6, 0x73);
    private static final byte[] RPI75 =
            asBytes(
                    0xfc, 0x1f, 0x8a, 0x66, 0xf4, 0x05, 0xcc, 0xb6, 0x3d, 0xc3, 0xe4, 0x82, 0x07, 0xda, 0x77,
                    0x88);
    private static final byte[] AEM75 = asBytes(0x9e, 0x4f, 0x1d, 0xb4);
    private static final byte[] RPI76 =
            asBytes(
                    0x0e, 0xac, 0xc2, 0x86, 0x31, 0xb1, 0x0f, 0x44, 0x98, 0x36, 0x86, 0x66, 0x13, 0x0f, 0xf0,
                    0xc9);
    private static final byte[] AEM76 = asBytes(0x5b, 0xe0, 0x4e, 0x9d);
    private static final byte[] RPI77 =
            asBytes(
                    0xe8, 0xdb, 0x4a, 0x46, 0x26, 0x38, 0x5a, 0xe6, 0xe3, 0xb2, 0x45, 0x1d, 0x0a, 0x66, 0xed,
                    0xbf);
    private static final byte[] AEM77 = asBytes(0x80, 0x6a, 0xf2, 0xf9);
    private static final byte[] RPI78 =
            asBytes(
                    0xc6, 0x00, 0x67, 0x8d, 0x4f, 0xbe, 0x92, 0x45, 0xad, 0x49, 0x73, 0xb1, 0xc8, 0x97, 0x1b,
                    0xc5);
    private static final byte[] AEM78 = asBytes(0x7a, 0xf8, 0xd4, 0xfd);
    private static final byte[] RPI79 =
            asBytes(
                    0x08, 0x19, 0x26, 0xc3, 0x61, 0x83, 0x4c, 0x5c, 0x1d, 0x43, 0x19, 0xb8, 0x40, 0xf3, 0x15,
                    0xae);
    private static final byte[] AEM79 = asBytes(0xfc, 0x8d, 0xdd, 0xd0);
    private static final byte[] RPI80 =
            asBytes(
                    0x1d, 0x82, 0x9e, 0xaf, 0xa0, 0x42, 0x32, 0xa6, 0xbb, 0x4d, 0x3c, 0x20, 0x22, 0xac, 0x3d,
                    0x0f);
    private static final byte[] AEM80 = asBytes(0xda, 0x88, 0x15, 0x68);
    private static final byte[] RPI81 =
            asBytes(
                    0x79, 0x8a, 0xbc, 0xe8, 0xc8, 0xf6, 0x25, 0x10, 0xde, 0x59, 0x5a, 0x99, 0x73, 0xfb, 0x21,
                    0x8e);
    private static final byte[] AEM81 = asBytes(0x13, 0x95, 0xcf, 0x7b);
    private static final byte[] RPI82 =
            asBytes(
                    0x61, 0xfc, 0x0d, 0xeb, 0x47, 0x08, 0x4f, 0xda, 0xf1, 0x48, 0x3e, 0x34, 0x2d, 0x73, 0xb1,
                    0x48);
    private static final byte[] AEM82 = asBytes(0xd9, 0xe2, 0x5d, 0xb6);
    private static final byte[] RPI83 =
            asBytes(
                    0x49, 0x69, 0xed, 0xd4, 0x0f, 0x3e, 0xab, 0x46, 0x8b, 0x8a, 0x8d, 0x49, 0x68, 0x5a, 0x4e,
                    0xdd);
    private static final byte[] AEM83 = asBytes(0xc1, 0xb2, 0x05, 0x97);
    private static final byte[] RPI84 =
            asBytes(
                    0x1d, 0x69, 0xd6, 0xd4, 0x15, 0x8a, 0x31, 0x8f, 0x1d, 0x9c, 0xaf, 0xba, 0x13, 0x58, 0x70,
                    0x17);
    private static final byte[] AEM84 = asBytes(0xeb, 0x53, 0x91, 0x99);
    private static final byte[] RPI85 =
            asBytes(
                    0xab, 0x7c, 0x61, 0xf1, 0xcc, 0xa8, 0x13, 0xfd, 0x36, 0xe8, 0xf1, 0xb1, 0xe5, 0xdf, 0x6a,
                    0x0f);
    private static final byte[] AEM85 = asBytes(0xc8, 0x1a, 0xb8, 0x54);
    private static final byte[] RPI86 =
            asBytes(
                    0x94, 0xf2, 0x4f, 0xde, 0xc1, 0x7a, 0xa3, 0x1f, 0x74, 0xf2, 0x02, 0xae, 0x4a, 0x68, 0x74,
                    0xbe);
    private static final byte[] AEM86 = asBytes(0x46, 0xee, 0x82, 0xdc);
    private static final byte[] RPI87 =
            asBytes(
                    0x6e, 0x5d, 0xe0, 0x25, 0x79, 0xd3, 0xdf, 0xb1, 0x94, 0xcc, 0x7b, 0xd4, 0x92, 0x70, 0x25,
                    0x3d);
    private static final byte[] AEM87 = asBytes(0x8a, 0x05, 0x28, 0xbd);
    private static final byte[] RPI88 =
            asBytes(
                    0xfe, 0x3e, 0x1e, 0x36, 0x21, 0x77, 0x3f, 0x18, 0x80, 0x40, 0xaa, 0x5d, 0xb3, 0xff, 0x1d,
                    0x4e);
    private static final byte[] AEM88 = asBytes(0x28, 0x03, 0x3a, 0xae);
    private static final byte[] RPI89 =
            asBytes(
                    0xd7, 0x37, 0x6e, 0x0d, 0x77, 0x25, 0x5d, 0xe2, 0x3d, 0x54, 0x0f, 0x02, 0x71, 0x83, 0xf1,
                    0xba);
    private static final byte[] AEM89 = asBytes(0xa0, 0x1c, 0xe7, 0x12);
    private static final byte[] RPI90 =
            asBytes(
                    0x64, 0xa7, 0x1e, 0x48, 0xa5, 0x0e, 0x7b, 0x5a, 0x37, 0xac, 0x91, 0x81, 0x6e, 0x2b, 0x0f,
                    0x53);
    private static final byte[] AEM90 = asBytes(0xc9, 0xcb, 0x8c, 0x70);
    private static final byte[] RPI91 =
            asBytes(
                    0x0f, 0x22, 0xa2, 0xc0, 0xb6, 0x99, 0xe1, 0x89, 0xd5, 0x9e, 0x30, 0xd1, 0x74, 0x5d, 0x67,
                    0xd3);
    private static final byte[] AEM91 = asBytes(0xdb, 0xe6, 0x8f, 0x47);
    private static final byte[] RPI92 =
            asBytes(
                    0xde, 0x87, 0x6b, 0xaf, 0x31, 0x22, 0x8e, 0x3b, 0x7f, 0xe0, 0xf0, 0x8e, 0x1f, 0x38, 0xea,
                    0x7b);
    private static final byte[] AEM92 = asBytes(0x4c, 0xb1, 0xd9, 0x4f);
    private static final byte[] RPI93 =
            asBytes(
                    0x8c, 0x69, 0x27, 0xbc, 0xf5, 0xf7, 0xae, 0xe1, 0xee, 0xd8, 0xab, 0xbe, 0x43, 0xe2, 0xe2,
                    0xd1);
    private static final byte[] AEM93 = asBytes(0x27, 0x74, 0x06, 0x32);
    private static final byte[] RPI94 =
            asBytes(
                    0xbc, 0x96, 0x83, 0x0f, 0x18, 0x2a, 0x72, 0xc8, 0x9e, 0x65, 0xce, 0xa9, 0xc4, 0x7d, 0x88,
                    0xc0);
    private static final byte[] AEM94 = asBytes(0xe8, 0xad, 0xeb, 0x6d);
    private static final byte[] RPI95 =
            asBytes(
                    0x7b, 0x2f, 0xbe, 0x74, 0x6d, 0xd2, 0xda, 0x86, 0xe9, 0x86, 0x6a, 0x0e, 0x6a, 0xad, 0xbc,
                    0x4d);
    private static final byte[] AEM95 = asBytes(0xe5, 0x3a, 0x5a, 0xc7);
    private static final byte[] RPI96 =
            asBytes(
                    0x93, 0xe2, 0x0f, 0x14, 0xa1, 0x3d, 0x56, 0x56, 0x75, 0x94, 0xaa, 0x96, 0x23, 0x50, 0xf1,
                    0x70);
    private static final byte[] AEM96 = asBytes(0x94, 0x32, 0x42, 0xa4);
    private static final byte[] RPI97 =
            asBytes(
                    0x9e, 0x38, 0x14, 0xf9, 0x51, 0xfa, 0x03, 0x79, 0x6b, 0x9a, 0x66, 0xf8, 0x9a, 0x9f, 0x40,
                    0x0d);
    private static final byte[] AEM97 = asBytes(0x06, 0x48, 0xdc, 0x89);
    private static final byte[] RPI98 =
            asBytes(
                    0x95, 0xfa, 0x09, 0x84, 0x5a, 0xa8, 0xd4, 0xb6, 0x00, 0x47, 0xfa, 0xf9, 0x9a, 0xeb, 0xca,
                    0x0c);
    private static final byte[] AEM98 = asBytes(0x85, 0x5e, 0x31, 0xb3);
    private static final byte[] RPI99 =
            asBytes(
                    0xef, 0x0a, 0x75, 0x79, 0x33, 0x18, 0x53, 0xb9, 0xeb, 0xc2, 0x50, 0xb4, 0xd6, 0xf3, 0xeb,
                    0xcc);
    private static final byte[] AEM99 = asBytes(0x9c, 0x1f, 0x07, 0xc2);
    private static final byte[] RPI100 =
            asBytes(
                    0x7f, 0xe9, 0xa9, 0x0d, 0xe0, 0x0c, 0x9f, 0x07, 0x37, 0xd3, 0xb4, 0x5f, 0xda, 0x65, 0x11,
                    0x15);
    private static final byte[] AEM100 = asBytes(0x98, 0x4e, 0x1f, 0xf3);
    private static final byte[] RPI101 =
            asBytes(
                    0x44, 0x3b, 0x7b, 0x5a, 0xb9, 0xa8, 0x6a, 0x1f, 0xee, 0x67, 0xe1, 0x8c, 0xb8, 0xc4, 0x07,
                    0x64);
    private static final byte[] AEM101 = asBytes(0xb3, 0xfb, 0xa7, 0xe1);
    private static final byte[] RPI102 =
            asBytes(
                    0xe8, 0xa6, 0xfa, 0x9a, 0x5c, 0xa9, 0xfb, 0x06, 0x1c, 0x4c, 0xdb, 0xe2, 0x17, 0xc6, 0x1d,
                    0x59);
    private static final byte[] AEM102 = asBytes(0x3b, 0x6d, 0x9d, 0xe8);
    private static final byte[] RPI103 =
            asBytes(
                    0x08, 0x7a, 0x08, 0x04, 0x06, 0x86, 0xfd, 0x63, 0x5e, 0xf3, 0x89, 0x75, 0x27, 0x41, 0xcc,
                    0x1f);
    private static final byte[] AEM103 = asBytes(0xf0, 0x85, 0x55, 0x1c);
    private static final byte[] RPI104 =
            asBytes(
                    0x58, 0x7c, 0x04, 0x86, 0x64, 0x4c, 0xeb, 0x2d, 0x0b, 0x7e, 0xbd, 0xd3, 0x9d, 0xd3, 0xa8,
                    0x60);
    private static final byte[] AEM104 = asBytes(0x0a, 0x6b, 0x32, 0x6e);
    private static final byte[] RPI105 =
            asBytes(
                    0xdd, 0x82, 0x7b, 0xa6, 0x0f, 0x8a, 0x35, 0xb1, 0xdd, 0x4e, 0x4c, 0xdf, 0xe4, 0x9c, 0x42,
                    0x63);
    private static final byte[] AEM105 = asBytes(0x81, 0xeb, 0x20, 0xe2);
    private static final byte[] RPI106 =
            asBytes(
                    0xcf, 0x23, 0x40, 0x00, 0x08, 0x0a, 0x4e, 0x8d, 0xa8, 0xfe, 0xb5, 0x33, 0xaa, 0x59, 0x04,
                    0xd3);
    private static final byte[] AEM106 = asBytes(0x34, 0xd8, 0x6e, 0xe5);
    private static final byte[] RPI107 =
            asBytes(
                    0xc5, 0x0f, 0xb1, 0xec, 0x3e, 0xf5, 0x4e, 0x91, 0x61, 0x78, 0xca, 0x9d, 0x56, 0xee, 0x4f,
                    0x5c);
    private static final byte[] AEM107 = asBytes(0xea, 0xea, 0xc3, 0xd2);
    private static final byte[] RPI108 =
            asBytes(
                    0xd4, 0x5f, 0xde, 0x46, 0xf4, 0x67, 0xd7, 0x6e, 0xd2, 0x8d, 0xd4, 0xd2, 0x49, 0x6d, 0xcb,
                    0x7f);
    private static final byte[] AEM108 = asBytes(0xea, 0xa6, 0x4a, 0x8e);
    private static final byte[] RPI109 =
            asBytes(
                    0xe9, 0xba, 0xf8, 0x1c, 0x96, 0x1f, 0x51, 0x0d, 0x08, 0xa6, 0x63, 0xc7, 0x52, 0x4f, 0x36,
                    0xdf);
    private static final byte[] AEM109 = asBytes(0x1e, 0x2c, 0xa3, 0x7b);
    private static final byte[] RPI110 =
            asBytes(
                    0xe3, 0xb0, 0x2c, 0xc0, 0x31, 0xe1, 0x44, 0xfa, 0xe6, 0x1a, 0x38, 0x99, 0x50, 0x1b, 0x49,
                    0x21);
    private static final byte[] AEM110 = asBytes(0x44, 0xc0, 0xd7, 0x06);
    private static final byte[] RPI111 =
            asBytes(
                    0xa7, 0x5e, 0xea, 0x58, 0x23, 0x2e, 0x73, 0x66, 0xce, 0xa1, 0xa0, 0xe7, 0x2d, 0xa0, 0xce,
                    0xc5);
    private static final byte[] AEM111 = asBytes(0x5a, 0x8c, 0x79, 0xb7);
    private static final byte[] RPI112 =
            asBytes(
                    0x6a, 0xd7, 0x62, 0x1f, 0xe1, 0xda, 0x01, 0x39, 0xff, 0x8b, 0xad, 0x7f, 0x37, 0x9c, 0xab,
                    0xf6);
    private static final byte[] AEM112 = asBytes(0x27, 0x9f, 0x16, 0xb1);
    private static final byte[] RPI113 =
            asBytes(
                    0x6f, 0xe2, 0xac, 0x45, 0xf6, 0x5c, 0x8a, 0xc6, 0x9f, 0xdc, 0x5e, 0xf7, 0xfa, 0x9f, 0xf7,
                    0xf0);
    private static final byte[] AEM113 = asBytes(0x4b, 0xd9, 0x07, 0xaa);
    private static final byte[] RPI114 =
            asBytes(
                    0x2f, 0xbe, 0xc6, 0x8f, 0xd2, 0x7d, 0xdd, 0xdb, 0x42, 0x23, 0x04, 0x4e, 0xfc, 0x77, 0x98,
                    0x51);
    private static final byte[] AEM114 = asBytes(0x2d, 0xf6, 0xc5, 0xeb);
    private static final byte[] RPI115 =
            asBytes(
                    0x32, 0xbf, 0x68, 0x8a, 0x7c, 0x83, 0x4b, 0xe1, 0xbf, 0xab, 0x7c, 0x8e, 0x0e, 0x58, 0x0a,
                    0xdb);
    private static final byte[] AEM115 = asBytes(0x66, 0x85, 0x2d, 0x43);
    private static final byte[] RPI116 =
            asBytes(
                    0xda, 0xe3, 0xa7, 0xd8, 0xc6, 0x24, 0x27, 0xb0, 0x9c, 0x0e, 0x7b, 0xbf, 0x48, 0x9d, 0x34,
                    0xbd);
    private static final byte[] AEM116 = asBytes(0x83, 0x6d, 0x3b, 0x95);
    private static final byte[] RPI117 =
            asBytes(
                    0x3c, 0x4b, 0x02, 0xbd, 0x5e, 0xd2, 0x8c, 0x67, 0x82, 0x9c, 0x97, 0x79, 0x10, 0x79, 0xaf,
                    0xd2);
    private static final byte[] AEM117 = asBytes(0x27, 0x35, 0xb9, 0x97);
    private static final byte[] RPI118 =
            asBytes(
                    0xe2, 0xfa, 0xea, 0xc3, 0xdb, 0xd1, 0x50, 0xec, 0x8e, 0xa8, 0xe7, 0xb3, 0xe5, 0xbb, 0x84,
                    0x54);
    private static final byte[] AEM118 = asBytes(0xd7, 0x1f, 0x97, 0xc2);
    private static final byte[] RPI119 =
            asBytes(
                    0x69, 0x94, 0x2a, 0x72, 0x13, 0xea, 0xf3, 0xc1, 0x4a, 0x69, 0x99, 0x6b, 0xa6, 0xc6, 0xbf,
                    0xeb);
    private static final byte[] AEM119 = asBytes(0x53, 0xbc, 0x4d, 0xb5);
    private static final byte[] RPI120 =
            asBytes(
                    0x1c, 0x5c, 0x4d, 0xd2, 0x54, 0x52, 0xe9, 0x7d, 0xd1, 0x87, 0xdd, 0x7c, 0xe1, 0xd1, 0xee,
                    0x81);
    private static final byte[] AEM120 = asBytes(0x48, 0xa4, 0xd3, 0x79);
    private static final byte[] RPI121 =
            asBytes(
                    0xfb, 0xf5, 0x60, 0x7a, 0x7c, 0x61, 0x2a, 0xce, 0xd1, 0x60, 0xe7, 0x55, 0xa9, 0x87, 0x26,
                    0x2d);
    private static final byte[] AEM121 = asBytes(0xb7, 0x8d, 0xc1, 0xf5);
    private static final byte[] RPI122 =
            asBytes(
                    0x3e, 0x2d, 0xe1, 0x30, 0x70, 0xf2, 0x74, 0x43, 0xd9, 0xba, 0x3e, 0xb4, 0x3f, 0x9a, 0x71,
                    0xea);
    private static final byte[] AEM122 = asBytes(0x58, 0x21, 0x70, 0xca);
    private static final byte[] RPI123 =
            asBytes(
                    0x8a, 0x12, 0xd2, 0x5f, 0x00, 0x6f, 0xab, 0x5a, 0x27, 0x07, 0xda, 0x9e, 0x6c, 0x4e, 0x96,
                    0xbe);
    private static final byte[] AEM123 = asBytes(0x93, 0x95, 0x94, 0xcc);
    private static final byte[] RPI124 =
            asBytes(
                    0x6f, 0xd9, 0x8c, 0x22, 0xe2, 0x27, 0x83, 0x8e, 0x6f, 0x67, 0x36, 0x97, 0x64, 0x43, 0x77,
                    0x25);
    private static final byte[] AEM124 = asBytes(0xc1, 0x4f, 0x5b, 0x11);
    private static final byte[] RPI125 =
            asBytes(
                    0x3d, 0xa2, 0x12, 0xae, 0xbd, 0xb7, 0x8b, 0xa8, 0x19, 0x80, 0x9d, 0x03, 0xc6, 0xcf, 0x56,
                    0xe2);
    private static final byte[] AEM125 = asBytes(0x30, 0x09, 0x12, 0xda);
    private static final byte[] RPI126 =
            asBytes(
                    0x8c, 0x48, 0xda, 0x73, 0xe2, 0x9e, 0xff, 0xc9, 0xb7, 0x4b, 0xb0, 0x97, 0x09, 0x6e, 0x0a,
                    0x0a);
    private static final byte[] AEM126 = asBytes(0xce, 0x79, 0xc5, 0x0a);
    private static final byte[] RPI127 =
            asBytes(
                    0xe5, 0x3c, 0x68, 0xb4, 0xb0, 0x1c, 0x68, 0xf3, 0x7e, 0x65, 0xa0, 0xdc, 0x8e, 0x67, 0xf4,
                    0x5d);
    private static final byte[] AEM127 = asBytes(0xe0, 0x4f, 0x38, 0x67);
    private static final byte[] RPI128 =
            asBytes(
                    0x83, 0x31, 0xdd, 0xe6, 0x36, 0x4b, 0x11, 0x95, 0x27, 0xaf, 0x76, 0xfe, 0xe1, 0x7a, 0xab,
                    0xcf);
    private static final byte[] AEM128 = asBytes(0x64, 0x18, 0x7b, 0xdf);
    private static final byte[] RPI129 =
            asBytes(
                    0x8c, 0x14, 0x47, 0x1f, 0x55, 0x71, 0x92, 0x63, 0x96, 0xdd, 0xe6, 0xf7, 0xb7, 0xb3, 0x5b,
                    0x56);
    private static final byte[] AEM129 = asBytes(0xe8, 0x7c, 0x05, 0xfd);
    private static final byte[] RPI130 =
            asBytes(
                    0x4e, 0xb6, 0xb2, 0xde, 0xb4, 0x0e, 0x5e, 0xc9, 0xbc, 0x39, 0x83, 0x81, 0x02, 0xa4, 0xf4,
                    0xf9);
    private static final byte[] AEM130 = asBytes(0x4e, 0x70, 0x83, 0x25);
    private static final byte[] RPI131 =
            asBytes(
                    0x77, 0xf2, 0x14, 0x1c, 0xef, 0xfd, 0x0a, 0xa3, 0xbe, 0xe4, 0xb6, 0x7c, 0x45, 0x0d, 0x9a,
                    0xa6);
    private static final byte[] AEM131 = asBytes(0xa1, 0x57, 0xeb, 0x59);
    private static final byte[] RPI132 =
            asBytes(
                    0x04, 0x3d, 0x78, 0xe2, 0x0c, 0xb5, 0x9c, 0x0b, 0xcb, 0x15, 0x78, 0xff, 0x93, 0xea, 0x54,
                    0x4a);
    private static final byte[] AEM132 = asBytes(0x8d, 0x30, 0x43, 0x2a);
    private static final byte[] RPI133 =
            asBytes(
                    0x65, 0xb8, 0xec, 0xc4, 0x56, 0x1c, 0x1c, 0xca, 0x05, 0x3d, 0x81, 0x4f, 0xfd, 0x89, 0x61,
                    0xd4);
    private static final byte[] AEM133 = asBytes(0x5b, 0x24, 0x38, 0x90);
    private static final byte[] RPI134 =
            asBytes(
                    0x32, 0xf2, 0x5a, 0x17, 0x24, 0xf2, 0xbd, 0xca, 0xd0, 0x5a, 0xbc, 0x14, 0x82, 0xe1, 0x32,
                    0x9e);
    private static final byte[] AEM134 = asBytes(0x74, 0x90, 0xd2, 0x11);
    private static final byte[] RPI135 =
            asBytes(
                    0x20, 0x3b, 0xa3, 0xf3, 0xf7, 0x23, 0x02, 0x66, 0xb9, 0x93, 0xb3, 0xee, 0x7b, 0x2d, 0x86,
                    0x08);
    private static final byte[] AEM135 = asBytes(0x1f, 0x01, 0x9d, 0xf2);
    private static final byte[] RPI136 =
            asBytes(
                    0xe5, 0xe7, 0xa4, 0x70, 0x69, 0x21, 0x6e, 0x1a, 0x88, 0x7b, 0x90, 0xef, 0x03, 0x94, 0xa3,
                    0x5c);
    private static final byte[] AEM136 = asBytes(0x99, 0xe6, 0x12, 0xb7);
    private static final byte[] RPI137 =
            asBytes(
                    0x3f, 0xfc, 0x8b, 0xb9, 0x1d, 0xbc, 0xd8, 0xee, 0x92, 0x49, 0x48, 0xf5, 0x08, 0x0b, 0x19,
                    0x0d);
    private static final byte[] AEM137 = asBytes(0x26, 0x4a, 0x57, 0x7e);
    private static final byte[] RPI138 =
            asBytes(
                    0x32, 0x37, 0x53, 0x91, 0x07, 0x7f, 0xbf, 0x76, 0x86, 0xba, 0xfa, 0x7d, 0xc1, 0x56, 0xbe,
                    0x1c);
    private static final byte[] AEM138 = asBytes(0xa8, 0x11, 0xba, 0x62);
    private static final byte[] RPI139 =
            asBytes(
                    0xa8, 0x90, 0x49, 0x65, 0xae, 0xc5, 0xdd, 0xb6, 0x55, 0xe9, 0x70, 0x07, 0xd2, 0x23, 0xdb,
                    0x48);
    private static final byte[] AEM139 = asBytes(0x2b, 0x48, 0x33, 0x21);
    private static final byte[] RPI140 =
            asBytes(
                    0x41, 0x5c, 0xf7, 0xaf, 0x1d, 0xc9, 0xfe, 0xac, 0xb3, 0x97, 0x84, 0x88, 0xf5, 0x04, 0x68,
                    0x93);
    private static final byte[] AEM140 = asBytes(0xe2, 0xef, 0x53, 0x7e);
    private static final byte[] RPI141 =
            asBytes(
                    0x86, 0xf8, 0xdd, 0xc0, 0xc9, 0x3e, 0x53, 0xe3, 0xa5, 0x82, 0xe6, 0x1f, 0x01, 0xf7, 0xdf,
                    0x2f);
    private static final byte[] AEM141 = asBytes(0x71, 0x1d, 0xbf, 0x6d);
    private static final byte[] RPI142 =
            asBytes(
                    0xa6, 0x5e, 0xf7, 0xba, 0x97, 0x52, 0xc4, 0x17, 0x3b, 0xa4, 0x8a, 0x33, 0x84, 0x9c, 0x5e,
                    0x52);
    private static final byte[] AEM142 = asBytes(0x59, 0x2d, 0xbc, 0xa8);
    private static final byte[] RPI143 =
            asBytes(
                    0xf4, 0x31, 0xb6, 0x2e, 0xcf, 0x44, 0x31, 0x02, 0xce, 0x4e, 0xd0, 0x40, 0x7d, 0xe5, 0x4b,
                    0xd4);
    private static final byte[] AEM143 = asBytes(0x12, 0x15, 0xe5, 0x7e);
    // ------------------------------------------------------------------------------

    public static final ImmutableList<AdvertisedData> ADVERTISED_DATA =
            ImmutableList.of(
                    new AdvertisedData(RPI0, AEM0),
                    new AdvertisedData(RPI1, AEM1),
                    new AdvertisedData(RPI2, AEM2),
                    new AdvertisedData(RPI3, AEM3),
                    new AdvertisedData(RPI4, AEM4),
                    new AdvertisedData(RPI5, AEM5),
                    new AdvertisedData(RPI6, AEM6),
                    new AdvertisedData(RPI7, AEM7),
                    new AdvertisedData(RPI8, AEM8),
                    new AdvertisedData(RPI9, AEM9),
                    new AdvertisedData(RPI10, AEM10),
                    new AdvertisedData(RPI11, AEM11),
                    new AdvertisedData(RPI12, AEM12),
                    new AdvertisedData(RPI13, AEM13),
                    new AdvertisedData(RPI14, AEM14),
                    new AdvertisedData(RPI15, AEM15),
                    new AdvertisedData(RPI16, AEM16),
                    new AdvertisedData(RPI17, AEM17),
                    new AdvertisedData(RPI18, AEM18),
                    new AdvertisedData(RPI19, AEM19),
                    new AdvertisedData(RPI20, AEM20),
                    new AdvertisedData(RPI21, AEM21),
                    new AdvertisedData(RPI22, AEM22),
                    new AdvertisedData(RPI23, AEM23),
                    new AdvertisedData(RPI24, AEM24),
                    new AdvertisedData(RPI25, AEM25),
                    new AdvertisedData(RPI26, AEM26),
                    new AdvertisedData(RPI27, AEM27),
                    new AdvertisedData(RPI28, AEM28),
                    new AdvertisedData(RPI29, AEM29),
                    new AdvertisedData(RPI30, AEM30),
                    new AdvertisedData(RPI31, AEM31),
                    new AdvertisedData(RPI32, AEM32),
                    new AdvertisedData(RPI33, AEM33),
                    new AdvertisedData(RPI34, AEM34),
                    new AdvertisedData(RPI35, AEM35),
                    new AdvertisedData(RPI36, AEM36),
                    new AdvertisedData(RPI37, AEM37),
                    new AdvertisedData(RPI38, AEM38),
                    new AdvertisedData(RPI39, AEM39),
                    new AdvertisedData(RPI40, AEM40),
                    new AdvertisedData(RPI41, AEM41),
                    new AdvertisedData(RPI42, AEM42),
                    new AdvertisedData(RPI43, AEM43),
                    new AdvertisedData(RPI44, AEM44),
                    new AdvertisedData(RPI45, AEM45),
                    new AdvertisedData(RPI46, AEM46),
                    new AdvertisedData(RPI47, AEM47),
                    new AdvertisedData(RPI48, AEM48),
                    new AdvertisedData(RPI49, AEM49),
                    new AdvertisedData(RPI50, AEM50),
                    new AdvertisedData(RPI51, AEM51),
                    new AdvertisedData(RPI52, AEM52),
                    new AdvertisedData(RPI53, AEM53),
                    new AdvertisedData(RPI54, AEM54),
                    new AdvertisedData(RPI55, AEM55),
                    new AdvertisedData(RPI56, AEM56),
                    new AdvertisedData(RPI57, AEM57),
                    new AdvertisedData(RPI58, AEM58),
                    new AdvertisedData(RPI59, AEM59),
                    new AdvertisedData(RPI60, AEM60),
                    new AdvertisedData(RPI61, AEM61),
                    new AdvertisedData(RPI62, AEM62),
                    new AdvertisedData(RPI63, AEM63),
                    new AdvertisedData(RPI64, AEM64),
                    new AdvertisedData(RPI65, AEM65),
                    new AdvertisedData(RPI66, AEM66),
                    new AdvertisedData(RPI67, AEM67),
                    new AdvertisedData(RPI68, AEM68),
                    new AdvertisedData(RPI69, AEM69),
                    new AdvertisedData(RPI70, AEM70),
                    new AdvertisedData(RPI71, AEM71),
                    new AdvertisedData(RPI72, AEM72),
                    new AdvertisedData(RPI73, AEM73),
                    new AdvertisedData(RPI74, AEM74),
                    new AdvertisedData(RPI75, AEM75),
                    new AdvertisedData(RPI76, AEM76),
                    new AdvertisedData(RPI77, AEM77),
                    new AdvertisedData(RPI78, AEM78),
                    new AdvertisedData(RPI79, AEM79),
                    new AdvertisedData(RPI80, AEM80),
                    new AdvertisedData(RPI81, AEM81),
                    new AdvertisedData(RPI82, AEM82),
                    new AdvertisedData(RPI83, AEM83),
                    new AdvertisedData(RPI84, AEM84),
                    new AdvertisedData(RPI85, AEM85),
                    new AdvertisedData(RPI86, AEM86),
                    new AdvertisedData(RPI87, AEM87),
                    new AdvertisedData(RPI88, AEM88),
                    new AdvertisedData(RPI89, AEM89),
                    new AdvertisedData(RPI90, AEM90),
                    new AdvertisedData(RPI91, AEM91),
                    new AdvertisedData(RPI92, AEM92),
                    new AdvertisedData(RPI93, AEM93),
                    new AdvertisedData(RPI94, AEM94),
                    new AdvertisedData(RPI95, AEM95),
                    new AdvertisedData(RPI96, AEM96),
                    new AdvertisedData(RPI97, AEM97),
                    new AdvertisedData(RPI98, AEM98),
                    new AdvertisedData(RPI99, AEM99),
                    new AdvertisedData(RPI100, AEM100),
                    new AdvertisedData(RPI101, AEM101),
                    new AdvertisedData(RPI102, AEM102),
                    new AdvertisedData(RPI103, AEM103),
                    new AdvertisedData(RPI104, AEM104),
                    new AdvertisedData(RPI105, AEM105),
                    new AdvertisedData(RPI106, AEM106),
                    new AdvertisedData(RPI107, AEM107),
                    new AdvertisedData(RPI108, AEM108),
                    new AdvertisedData(RPI109, AEM109),
                    new AdvertisedData(RPI110, AEM110),
                    new AdvertisedData(RPI111, AEM111),
                    new AdvertisedData(RPI112, AEM112),
                    new AdvertisedData(RPI113, AEM113),
                    new AdvertisedData(RPI114, AEM114),
                    new AdvertisedData(RPI115, AEM115),
                    new AdvertisedData(RPI116, AEM116),
                    new AdvertisedData(RPI117, AEM117),
                    new AdvertisedData(RPI118, AEM118),
                    new AdvertisedData(RPI119, AEM119),
                    new AdvertisedData(RPI120, AEM120),
                    new AdvertisedData(RPI121, AEM121),
                    new AdvertisedData(RPI122, AEM122),
                    new AdvertisedData(RPI123, AEM123),
                    new AdvertisedData(RPI124, AEM124),
                    new AdvertisedData(RPI125, AEM125),
                    new AdvertisedData(RPI126, AEM126),
                    new AdvertisedData(RPI127, AEM127),
                    new AdvertisedData(RPI128, AEM128),
                    new AdvertisedData(RPI129, AEM129),
                    new AdvertisedData(RPI130, AEM130),
                    new AdvertisedData(RPI131, AEM131),
                    new AdvertisedData(RPI132, AEM132),
                    new AdvertisedData(RPI133, AEM133),
                    new AdvertisedData(RPI134, AEM134),
                    new AdvertisedData(RPI135, AEM135),
                    new AdvertisedData(RPI136, AEM136),
                    new AdvertisedData(RPI137, AEM137),
                    new AdvertisedData(RPI138, AEM138),
                    new AdvertisedData(RPI139, AEM139),
                    new AdvertisedData(RPI140, AEM140),
                    new AdvertisedData(RPI141, AEM141),
                    new AdvertisedData(RPI142, AEM142),
                    new AdvertisedData(RPI143, AEM143));

    private TestVectors() {
    }
}
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.samples.ensnippets">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
#
# Copyright 2020 Google LLC
#
# 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.
#

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_google_samples_ensnippets_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.ensnippets

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*

/**
 * This is NOT a usable application and was included only as convenience during development
 * to ensure the code would compile.
 *
 * There are several tests included in the "androidTest" folder that can be run once the APK
 * can be built.
 */
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Example of a call to a native method
        sample_text.text = stringFromJNI()
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    external fun stringFromJNI(): String

    companion object {
        // Used to load the 'native-lib' library on application startup.
        init {
            System.loadLibrary("native-lib")
        }
    }
}
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
        <aapt:attr name="android:fillColor">
            <gradient
                android:endX="85.84757"
                android:endY="92.4963"
                android:startX="42.9492"
                android:startY="49.59793"
                android:type="linear">
                <item
                    android:color="#44000000"
                    android:offset="0.0" />
                <item
                    android:color="#00000000"
                    android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
        android:fillColor="#FFFFFF"
        android:fillType="nonZero"
        android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
        android:strokeWidth="1"
        android:strokeColor="#00000000" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path
        android:fillColor="#3DDC84"
        android:pathData="M0,0h108v108h-108z" />
    <path
        android:fillColor="#00000000"
        android:pathData="M9,0L9,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,0L19,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,0L29,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,0L39,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,0L49,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,0L59,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,0L69,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,0L79,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M89,0L89,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M99,0L99,108"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,9L108,9"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,19L108,19"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,29L108,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,39L108,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,49L108,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,59L108,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,69L108,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,79L108,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,89L108,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M0,99L108,99"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,29L89,29"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,39L89,39"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,49L89,49"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,59L89,59"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,69L89,69"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M19,79L89,79"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M29,19L29,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M39,19L39,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M49,19L49,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M59,19L59,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M69,19L69,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
    <path
        android:fillColor="#00000000"
        android:pathData="M79,19L79,89"
        android:strokeWidth="0.8"
        android:strokeColor="#33FFFFFF" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background android:drawable="@drawable/ic_launcher_background" />
    <foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<resources>
    <color name="colorPrimary">#6200EE</color>
    <color name="colorPrimaryDark">#3700B3</color>
    <color name="colorAccent">#03DAC5</color>
</resources>
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<resources>
    <string name="app_name">Exposure Notifications Snippets</string>
</resources>
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    ext.kotlin_version = "1.3.72"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.12"

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
# Exposure Notification: Risks and Mitigations FAQ

This document explains the key design decisions that affect the privacy and
security of the Google/Apple Exposure Notifications system (ENS), the risks that
were evaluated, and the mitigations that are in place for these risks. Details
of the ENS design can be found
[here](https://www.google.com/covid19/exposurenotifications/).

# Key design decisions and constraints relevant to security and privacy

## Decentralized architecture

The design of an effective exposure notification system requires trade-offs
between efficacy, privacy, and security. A common way of categorizing contact
tracing and Exposure Notification designs is between:

1.  **Centralized.** A central service makes decisions about whether to
    notify someone if they have been exposed or not. Centralized designs
    require devices to share a list of observed Bluetooth identifiers with a
    central notification service.
2.  **Decentralized.** The user's device decides locally whether to notify
    someone if they have been exposed or not. Decentralized designs require
    devices to receive a list of all COVID-positive users' broadcast Bluetooth
    identifiers.

The Google/Apple design uses a decentralized architecture because:

1.  Devices of COVID-negative users do not share any information about
    social interactions, location, or infection status. Decentralized designs
    provide better privacy protection for COVID-negative participants compared
    to centralized designs, since they do not require sharing any information
    with a central service unless a user declares themselves COVID-positive.
    This is especially important since we assume that the vast majority of
    participants will be COVID-negative.
    In contrast, centralized designs have the property that any party with
    access to the central service has access to observed Bluetooth Low Energy
    (BLE) identifiers—and therefore information about social interactions—for
    all participants, including those who have not declared themselves as
    COVID-positive.
2.  For COVID-positive users, the information shared with the server in a
    decentralized model is their COVID-positive status rather than any
    information about social interactions: decentralized designs require
    sharing broadcast Rolling Proximity Identifiers (RPIs) of COVID-positive
    users with all participating devices (usually via a central service).
    Centralized designs expose users' social interactions to a central service
    in order to receive notifications. Even if care is taken not to store
    information on relationships, the centralized designs inherently risk that
    this information may be obtained by an adversary.
    COVID-positive status data presents less incremental risk to the user and
    is less permanent than a person's social interactions: see
    [here](#covid-positive) for more details.
3.  Centralized designs also expose COVID-positive status. While centralized
    designs can minimize information revealed to the public at large, and make
    such attacks more difficult, they cannot prevent attacks that identify
    COVID-positive users entirely.
    In a centralized model an adversary can emit Bluetooth IDs in the vicinity
    of a device whose owner has been identified via other means, for example,
    by using vehicle number plate recognition. Once this device reports its
    owner as COVID-positive, the adversary's device will receive an exposure
    notification, which can be tied to the identified user. If they want to do
    this multiple times, however, they would need to have a single Healthcare
    Authority account, that is notification channel, per identified user.
    Healthcare Authority diagnosis servers could implement defenses against
    Sybil attacks using multiple accounts, but this could put legitimate users
    at higher risk of being falsely excluded from receiving exposure notifications.

## Location

Location data from the device is not used by the Google/Apple EN API in any
way.

Healthcare Authority applications using the Exposure Notification API are also
subject to the condition that applications do not make use of location APIs, and
do not record or store location information from the device.

EN respects the Android requirement for all Bluetooth scanning services to
enable the device location setting. This requirement has been present in the
Android platform since 2015, and is designed to protect the user's privacy by
preventing apps from using nearby Bluetooth beacons to locate the user when the
user has explicitly turned off all location for the device.

Enabling this setting by itself does _not_ mean that an application is using the
device's location: the location runtime permission is _also_ required for any
application to access the device's location. Android users can still decide
whether any app they've installed has permission to access the location of their
device, even if the device location setting is on.

## Bandwidth usage

The decentralized model requires a larger amount of data to be downloaded by
each device. In the EN, devices download a highly compressed version of 14 days
worth of Rolling Proximity Identifiers of every COVID-positive user. Using daily
exposure keys achieves approximately 100x compression by allowing the 15 minute
RPIs to be regenerated from daily seeds. We estimate that 14 days' worth of
daily keys for 100,000 COVID-positive people is 22.4 MB. We recommend that EN
apps download keys when the device is connected to wifi and power if possible.

## Use of connectionless protocol

The ENS uses a connectionless protocol. In our experience, protocols that
require BLE connections or other forms of two-way interactions between
participants' phones result in error-prone implementations, and can have
significant negative effects on battery life. This led to a decision to use
connectionless BLE advertisements as part of the protocol. One consequence of
this is that the size of proximity identifiers is constrained to 20 bytes.

# Risks and Mitigations

The following is an analysis of security and privacy risks to the system and its
users that were considered and mitigated to the extent possible within the above
design constraints.

## Disruption

**Concern**

Adversaries may attempt to disrupt the EN to create fraudulent exposure
notifications. Such fraudulent exposure notifications may result in
individuals or targeted groups unnecessarily self-quarantining. Disruptions
could also decrease the effectiveness of the system and increase its cost
of operation to both users and networks by, for example, increasing
bandwidth costs for users.

### RPI Spoofing using reported diagnosis keys

**Concern**

An adversary may try to use diagnosis keys downloaded from the server to
generate and broadcast proximity identifiers in an attempt to create false
ENs to users receiving them.

**Mitigations**

The EN implementations in both Android and iOS manage temporary exposure
keys on behalf of client apps. When a user tests positive, the user can
instruct their client app to upload their keys.

Both Android and iOS only return keys that are no longer in use. This means
that only expired keys are uploaded and revealed publicly. The matching
algorithm prevents a proximity identifier generated by expired keys from
leading to Exposure Notifications, mitigating this replay attack scenario.

### Relaying RPIs

**Concern**

An adversary could record proximity identifiers being broadcast by
legitimate users and rebroadcast them elsewhere. This could be done for
users likely to be diagnosed soon thereafter—for example, near a hospital
or testing center—and then rebroadcast to target a large number of users.
Alternatively, a large number of collected RPIs could be rebroadcast over a
large group, increasing the notification rate for that group in line with
the increased broadcast rate, effectively simulating being in a crowd.

A second variant, where the adversary is able to compromise the test
verification process, is to broadcast RPIs from Temporary Exposure Keys
(TEKs) that are subsequently uploaded to a diagnosis key server.

**Mitigations and considerations**

We acknowledge that a sophisticated adversary could mount relay attacks
against our system but we note that:

*   Any potential attack would require relayed broadcasts to persist for
    as long as the Healthcare Authority app threshold for an exposure, for
    example, 5-10 minute duration of close proximity to the target devices.
    This would require high powered antennas for most targets, which both
    increases the complexity of an attack and makes it more detectable.
*   Potential device-based malware that rebroadcasts BLE RPIs
    would have to be deployed at large scale to be successful. Any such malware
    detected during Google Play's rigorous review process would be removed.
*   Deploying fixed BLE listeners and broadcasters requires non-trivial
    physical setup and a source of power, which puts adversaries and their
    equipment at risk of discovery.
*   A hypothetical attack of this type could readily be detected by
    monitoring the rate of exposure notifications for anomalous changes that
    are inconsistent with an epidemiologically realistic model and/or data
    from other sources, such as manual contract tracing.

    For example, if mutually independent sources—e.g. test results and
    manual contact tracing data—show the infection rate is stable at 0.1% in
    a specific area, but exposure notifications increase tenfold, this would
    be a clear abuse signal.
*   A mechanism to verify that keys uploaded are tied to a positive test
    result is strongly recommended to all Health Authorities to limit the
    ability to upload fake diagnosis keys.
    Google has published a reference verification service design
    [here](https://developers.google.com/android/exposure-notifications/verification-system)
    and open source code
    [here](https://github.com/google/exposure-notifications-verification-server).
    This design is built with two foundational privacy goals in mind:

    *   The diagnosis key server should not learn the identity of a
        COVID-positive individual
    *   The verification server and the Healthcare Authority should not
        learn the TEKs of the COVID-positive individual

#### Additional considerations

Relay attacks are well understood and have been studied for many years:
all mitigations require either location information, which creates a new
privacy risk for ENS, or precise timing information. Timing-based solutions
are not feasible in this system since BLE packets can be relayed almost
instantaneously over the internet. Matching in ENS must be tolerant to
legitimate variations in timing such as processor speed. Distance-bounding
protocols that also rely on precise timing were discounted because they
require a 2-way protocol (see [connectionless
protocols](#use-of-connectionless-protocol)) and the high degree of precision
required is not supported by most BLE device firmware. That said, we
continue to evaluate potential mitigations within the parameters of our
design goals.

**Concern**

Associated Encrypted Metadata (AEM) TX (transmit) power could be altered as
part of a relay attack. We do not believe that TX power authentication
would be a useful defense against relay attacks for the following reasons:

*   TX power authentication doesn't protect against the use of a
    high-power antenna positioned to cover a large area with a signal strength
    indicative of proximity. This is because the system is designed to be
    highly tolerant of differences between TX-power and received signal
    strength indicator (RSSI) caused, for example, by both devices being
    in-pocket. The Android ecosystem also has a diverse range of hardware: some
    Android devices transmit signals as much as 30dB weaker than others. The
    required system tolerance to differences in transmit power means that
    relaying a packet from such a weak device on a higher transmit power device
    would already allow the higher power device to appear nearby without
    altering the TX field.
*   TX power authentication would not prevent the deployment of malware on
    phones to perform relay packets; app-store and OS policy are more effective
    to prevent collection of EN packets by third party apps.
*   In this context, encrypting the AEM prevents an adversary from joining
    between RPIs coming from the same device (using device-specific TX power
    as a source of entropy).


### Server data pollution

**Concern**

An adversary broadcasts RPIs based on adversary-chosen diagnosis keys to
many targets, then fraudulently uploads the keys as if they belonged to a
legitimately diagnosed individual. Even without broadcasting RPIs based on
these keys, this could be used to disrupt the service since an excessive
number of keys would result in unacceptable bandwidth and CPU usage to
download and match keys on devices.

**Mitigations**

*   We strongly recommend that all Health Authorities implement a
    mechanism to verify that keys uploaded are tied to a positive test result.
    This will limit the ability to upload fake diagnosis keys. See [Relaying
    RPIs](#relaying-rpis) for more details
*   Servers are also strongly encouraged to implement standard denial of
    service/abuse prevention mechanisms.

## Learning COVID-positive status

### Re-identification

**Concern**

During the 14 day exposure window, an adversary could collect a target's
Rolling Proximity Identifiers and then correlate them with published
diagnosis keys. The adversary (who could be a legitimate participant in EN)
could then identify the source of the exposure based on information
external to the system, for example, using knowledge of who was present at
the same time and place.

To succeed, the adversary must simultaneously identify the specific target
(visually, for example via number plate recognition, at a border control
point, physical access controls to buildings) and record the RPI
transmitted by an EN app on the target's mobile device. If both can be done
at the same time, the adversary can discover that the target was
COVID-positive after they upload their Temporary Exposure Keys (TEKs).
If multiple users of the EN app are within range, the adversary will have
to distinguish between several users' RPIs, though signal strength could be
used to narrow identification. Conclusive identification of the target's
diagnosis status might require several periods of RPI capture during the
same day. TEKs are randomly and independently generated using a
cryptographic random number generator (see
[EN Cryptography Specification](https://blog.google/documents/69/Exposure_Notification_-_Cryptography_Specification_v1.2.1.pdf)).
Therefore, an adversary would have to carry out re-identification attacks
separately for each day.

**Mitigations**

The core purpose of an EN system is to learn whether any COVID-positive
people have been close enough for long enough to put the device owner at
risk of infection. Without this ability, the system doesn't do what it was
designed to. The alternative is not to operate an EN system at all. While
ENS apps do not have access to scanned RPIs via the ENS APIs, it's not
possible to prevent a sophisticated adversary from capturing BLE RPIs using
a dedicated eavesdropping device.

<a id="covid-positive">With</a> this in mind, an important motivation in deciding between centralized
and decentralized has been that COVID-positive status data presents less
incremental risk to a person, and is less permanent than a person's social
interactions, which are exposed by a centralized approach. For example:

*   COVID-positive status may only be accurate for a few days after
    someone reports their diagnosis keys
*   Many celebrities have publicly shared their COVID-positive status on
    social media
*   Governments keep records of COVID-positive individuals—separate from
    diagnosis key servers—whether or not an ENS is in operation
*   COVID-positive status is easily observed by other means; for example,
    neighbors or the press can often observe if someone has been hospitalized
*   Some already operational government contact tracing discloses a person's
    COVID-positive status to their contacts

#### Additional considerations

**Split key** approaches are one proposed mitigation: multiple pieces
(cryptographic secret shares) of an RPI, broadcast at different times, must
be observed before a match with a reported diagnosis key is possible. To
succeed, a re-identifying adversary would have to be near the target for
long enough to successfully match. We have not implemented such a scheme
for several reasons:

*   The requirements for an adversary seem no different from the
    Healthcare Authority-defined criteria for an exposure
*   The risk of missing relevant contacts increases as the split key window
    increases. Our design avoids situations where valid contacts are otherwise lost.
*   The encounter duration threshold for a valid contact can be set by an EN
    app's Healthcare Authority at matching time. A split key solution requires
    this threshold to be decided at broadcast time, which reduces the
    flexibility to modify duration thresholds, for example, in roaming scenarios.

We note that centralized solutions are also vulnerable to a targeted
re-identification attack. In the centralized case, the adversary broadcasts
RPIs in proximity to their target, shares the RPIS with the central
service, and then observes whether or not they receive an Exposure
Notification.

### Network traffic analysis

**Concern**

An adversary could learn about infection status by observing network
traffic between devices and servers, including diagnosis servers.

**Mitigations**

*   Use Transport Layer Security (TLS) to protect the integrity and
    confidentiality of network data.
*   We recommend that Healthcare Authority apps make randomized requests to
    servers to prevent an adversary from concluding a user was diagnosed based
    on observing network traffic to the diagnosis server that only happens upon
    diagnosis. This is implemented in the Google open-source sample app
[here](https://github.com/google/exposure-notifications-android/blob/master/app/src/main/java/com/google/android/apps/exposurenotification/network/DiagnosisKeyUploader.java#L115).  

### Diagnosis server compromise

**Concern**

An adversary with access to the data on the diagnosis server could use this
information to potentially identify users.

**Mitigations**

Diagnosis keys do not identify users. They are randomly generated and not
inherently linked to individuals' identities. Each person's diagnosis keys
are generated independently each day and are unrelated.
Google's API Terms of Service forbid associating diagnosis keys with
personally identifiable information, or with other diagnosis keys from the
same device, except temporarily for verification purposes to support
certain already deployed verification flows. We also recommend that
diagnosis server operators limit retention of potentially identifiable
information including, but not limited to, server logs that contain IP
addresses.

### Compromised mobile device

**Concern**

EN apps store information on-device that would allow an adversary to
determine the device user's diagnosis status. For example, whether or not
the user is participating in EN and hasn't yet uploaded diagnosis keys.

**Mitigations**

Android includes multiple strong layers of defense against device
compromise, including [Google Play
Protect](https://www.android.com/play-protect/). If an adversary is able to
compromise the device, much more sensitive information is available,
including the user's contacts, messaging history, emails, and photos.

### Forensics and physical access to devices

**Concern**

An adversary with physical access to the device could extract historical
information such as observed RPIs or the TEKs used to generate broadcast
RPIs

**Mitigations**

*   Whenever a device supports full-disk encryption (FDE) or file-based
	encryption (FBE), all data stored on disk is encrypted using this
	standard mechanism by default. For devices that do not support either of
	these, encryption-at-rest of data has no benefit because the key would
	need to be stored in plaintext alongside the data. Such encryption is
	mandatory on all devices running Android 10 and above and on all devices
	running Android 6.0 or later, excluding low-RAM devices and devices on
	which AES bulk encryption performance is below 50MB/s. 

*   Encryption of data sent to the server and stored by the HA app is
	the responsibility of these apps. Google’s sample app uses HTTPS and the
	default FBE implementation available. Disabling HTTPS requires the HA to
	set an explicit network security configuration setting in the APK
	manifest.

*   TEKs generated by the device are stored for 14 days before being
	deleted by a daily maintenance process. The same is true for RPIs
	scanned by the device and matches detected. Note that Healthcare
	Authorities are also responsible for handling TEKs when reporting a
	positive diagnosis. In this case, storage and deletion are under the
	control of the HA app.

*   Even when an attacker has physical access to local storage, platform
	encryption protects the data. If an attacker were to try and compromise
	system integrity (rooting) this would require unlocking the bootloader,
	which on all modern Android devices automatically wipes user data as a
	security precaution.

*   Android devices do not have swap partitions. Where the related
	feature ``zram writeback`` is used, compressed RAM is written to the
	``/data/per_boot`` directory, which is encrypted with a newly generated key
	on each boot and stored only in RAM.

*   Devices using a physical secure element (SE) and implementing
	File-Based Encryption (FBE, introduced in Android 7.0 and mandatory on
	new devices since Android 10) store the Key Derivation Function (KDF)
	secret in the SE and destroy it upon Factory Data Reset (FDR), thus
	rendering data irrecoverable;

*   Devices using the SE in general provide non-recoverability guarantees
	that are at least as strong as the physical and logical integrity of the
	SE.

*   Even if they do not have a secure element (SE), devices running
	either FBE or full-disk encryption (FDE) generally require at least the
	physical compromise of the flash memory to recover any data after FDR.

## Learning social interactions

**Concern**

An adversary could potentially identify a participant of EN as the origin
of an Exposure Notification. This would also reveal social interactions
since it implies the two were in proximity.

An adversary with access to a device's EN APIs could feed a custom match
query to the provideDiagnosisKeys API with a known, identified diagnosis
key. If the API returns a match, the adversary knows that the individual
with the diagnosis key exposed the device owner, that they met, and for how
long.

An adversary could also feed a group of diagnosis keys to the API to learn
if a user associated with the group of keys exposed the device owner. TEK
metadata could be used to increase the effectiveness of this potential
attack by observing matching results for a subset of diagnosis keys, tagged
using TEK metadata. This could involve chaining between multiple
observations if metadata state changes are possible.

Location variant: If the RPIs associated with that group of diagnosis keys
were broadcast from a fixed location known to the adversary, the same
attack could be used to learn about a target's location.

**Mitigations**

This type of potential attack would require connecting a user's identity
with any diagnosis keys used in the attack. Mitigations against this attack
include:

1. Logging hashes of queries and making them exportable in the UI with the
user's consent. These keys can be pooled for analysis to detect targeted
attacks.

2. The diagnosis server must sign all key bundles used in matching. Invalid
signatures will result in an error upon matching. This prevents an attack
via a compromised app that submits malicious queries. This mechanism also
lets servers control the queries made and rate-limit them.

3. Starting with Exposure Notification API v1.5, Healthcare Authority apps
can only make six queries for `ExposureWindow` per day, reducing the
risk of brute force attacks of this type.

4. Metadata state changes are limited to only one per key. Such state
changes are used, for example, to allow self-reported keys to transition to
a confirmed diagnosis.

5. The API can only be called from a limited set of known, approved apps.
This means that an adversary must either have root-exploited the device or
has compromised an Exposure Notification app's code signing keys.

6. Connecting diagnosis keys with a user's identity for any purpose other
than test verification is against
[Google's additional terms of service for ENS apps](https://blog.google/documents/72/Exposure_Notifications_Service_Additional_Terms.pdf).

#### Additional considerations

Requiring a minimum query size would not help mitigate this attack
because the API has no way to differentiate between a valid and invalid
diagnosis key. An adversary could fill the rest of the query with fake or
COVID-negative keys.

## Tracking

### Tracking COVID-positive users for the rolling window period using the Temporary Exposure Key

**Concern**

An adversary with access to the downloaded TEKs could link a
COVID-positive user's RPIs for the duration of a rolling window period.
Using BLE sniffers in multiple places, an adversary could link together
different observations of the user over the rolling window period but not
longer.

See also [Learning Social Interactions: location
variant](#learning-social-interactions) above.

**Mitigations and considerations**

Linking Rolling Proximity Identifiers with a Temporary Exposure Key for the
duration of the Rolling Window is a network compression mechanism that
reduces bandwidth costs for all users by approximately 100X. (See
[Bandwidth Usage](#bandwidth-usage) above). The impact of this
effect is greater in high-population countries where bandwidth is
relatively costly. Our design must balance cost and network load with
limiting trackability. We note that:

1.  This attack would allow trackability **only** **for declared
    COVID-positive users** and only over the rolling period window, which is 24
    hours in EN version 1.5. Users are clearly informed and can opt-out when
    uploading TEKs following a positive diagnosis.
2.  For users who have not consented to upload their diagnosis keys
    following a positive COVID test result, an adversary cannot learn any more
    about the user than they already can by sniffing BLE packets.

    Rolling Proximity Identifiers are designed to be no more linkable than
    standard Bluetooth broadcast packets, which include a frequently rotating
    MAC address. Bluetooth Low-Energy (BLE) MAC addresses rotate at least every
    15 minutes, at the same time as the EN RPI. Therefore, a COVID-negative
    person's device can at most be recognized for 15 minutes by another device
    within physical BLE range. RPI intervals are synchronized with these
    rotating Bluetooth MAC addresses to prevent linking alternately based on
    MAC address and RPI.

    By restarting the BLE interface on RPI reset, EN creates a random rotation
    interval for all devices that is synchronized with BLE MAC rotation. This
    prevents the BLE MAC being used to bridge the RPI rotation.
3.  This attack would require a large network of BLE sniffers.
4.  Unless the adversary has an independent means (for example, a camera
    network) of identifying users whose devices are transmitting BLE packets,
    the adversary will not learn the identity of COVID-positive users. If an
    adversary has a mechanism for identifying users in proximity, then they
    could already use this to track the user's movements. Using EN does not
    significantly increase tracking risks.
5.  To reduce the impact on the user's network bandwidth, the rolling period
    window is set at 24 hours but can be reduced in lower infection rate
    scenarios to lessen risks.
6.  The movements and interactions of COVID-positive users are revealed to a
    much greater extent by some manual contact tracing methods.
7.  Google Play’s policies forbid the malicious use of BLE scanning, and
    Play’s rigorous review processes are designed to detect it. Any app
    found to be explicitly capturing BLE RPIs will be removed.
	
#### Additional considerations

**Cuckoo filter approach as a mitigation**

One mitigation that has been proposed
[[see DP3T White Paper](https://github.com/DP-3T/documents/blob/master/DP3T%20White%20Paper.pdf)]
is to distribute RPIs using a cuckoo filter rather than using temporary
exposure keys and a pseudo-random function as in the current EN design.

A cuckoo filter approach trades a reduction in bandwidth usage for a
higher number of false-positives and reduced trackability. We decided
against this approach because achieving an acceptable number of
false-positives would require excessive bandwidth and device storage
consumption.

An in-depth analysis of the tolerable false-positive rate as part of our
initial design led us to use 128-bit RPIs. Reaching a similarly low
false-positive rate would make the use of cuckoo filters unfeasible from a
bandwidth and storage perspective, given that the cuckoo filter is used for
matching with RPIs, 144x more of them than Temporary Exposure Keys. For
example, even allowing for a significantly higher false-positive rate for
large deployments, for a daily infection rate of 60,000 with 50 million
users, the download sizes required by each user would be over 1 GB per day.

Unlike our current design, another concern with the use of cuckoo filters
is that a match can be caused without the knowledge of the TEK that
generated that RPI value. This means that clients performing RPI matching
cannot verify that a corresponding TEK from which they were derived was
released, or when they were supposed to be broadcast. This makes the time
window for messages to be replayed longer.

Using a cuckoo filter as a first stage match with a full check following
would leak information about the user's social interactions to the server,
which goes against a core privacy property of the system.

### Bluetooth-based tracking

**Concern**

Switching on Bluetooth for users who had it switched off enables any
tracking risk present with the standard Bluetooth stack.

**Mitigations and considerations**

The incremental risk of turning Bluetooth on to enable EN should be
considered small because:

*   All BLE packet MAC addresses rotate at least every 15 minutes in sync
    with the EN RPI. At most, a person's device can be recognized for 15
    minutes by another device within physical BLE range.
*   7-15 minute rotating BLE advertising IDs are used by Android devices
    starting with Android Marshmallow, that is approximately 80% of devices in
    use. A large percentage of pre-Marshmallow devices do not have BLE, and
    would therefore not be able to run EN. Bluetooth Classic uses a fixed MAC
    address but this is only advertised if the device is in discoverable mode,
    that is when Bluetooth settings are open or when an app has triggered this
    mode, with a short timeout. Some manufacturers' Android devices enable
    Bluetooth Classic in discoverable mode whenever Bluetooth is turned on. We
    have notified, and are working with, affected manufacturers to fix this
    vulnerability. In the meantime, on Android, we turn off discoverability the
    first time EN is run, and every 24 hours thereafter.
*   Unless the adversary has an independent means of identifying users whose
    devices are transmitting BLE packets ( for example, a network of cameras)
    an adversary could not learn the identity of users or anything about who
    associates with whom. If an adversary has a mechanism for identifying users
    in proximity, then they can already use this to track the users' movements,
    and BLE does not introduce additional risk.

**Bridging rotations at MAC/RPI boundaries**

MAC and RPI rotations are designed to protect against adversaries
observing the device’s BLE emissions across different locations and
times, rather than continuously. For example, this mitigation protects
against an adversary putting BLE sniffers in multiple train stations
across a city, or against a store owner who links a user’s prior
purchases to their path around the store.

Conversely, protecting against an adversary who can continuously collect
every BLE frame emitted by a device is not a design goal for either MAC
or RPI rotation.

*   This attack requires continuous proximity; an adversary would need
    to have an independent means of tracking the user’s location, or a
    spatially continuous network of sensors. For example, in order to track
    a device leaving from home at 0900, going to a coffee shop at 0930, and
    then to the library at 1100, the attacker must observe all 0915, 0930,
    0945, 1000, … 1100 rotation events. If they could only observe the 0900
    and 1100 rotations they would not be able to deduce that those were the
    same device. 

*   While this means that such attacks aren’t useful for an adversary,
    it’s worth noting that the ability to bridge between temporally adjacent
    broadcast IDs is inherent in the use of any RF protocol since: 

    *   Adjacent timestamps can be used to link sources across frames

    *   Signal strength can be assumed to be invariant between adjacent frames
        and can therefore also be used to link sources across frames.

For the sake of transparency, we note that this issue was confirmed on a
subset of Android devices globally.  These issues likely resulted from
how certain OEMs have implemented Bluetooth since, for the reasons noted
above, the [Android Compatibility Definition Document](https://source.android.com/compatibility/10/android-10-cdd#7_4_3_bluetooth) (CDD) does not
require rotation in sync.  After extensive testing, a change to EN has
nevertheless been rolled out that removes this opportunity for
device-specific misbehavior with respect to EN for all devices. The RPI
is now set to a globally fixed value for a small number of BLE frames
surrounding the RPI rollover.

As noted above, Google Play’s policies forbid the malicious use of BLE
scanning, and Play’s rigorous review processes are designed to detect
it. Any app found to be explicitly capturing BLE RPIs will be removed.

### Linking diagnosis keys through export file analysis

**Concern**

An adversary could attempt to link Diagnosis Keys from different days—which
are independently randomly generated and _a priori_ not linkable—based on
analysis of Diagnosis Key batches served by the Diagnosis server.

**Mitigations**

*   This may be feasible specifically in situations where very few
    individuals are diagnosed positive in a given timeframe. If a diagnosis key
    batch only contains one diagnosis key for each day, the keys in the batch
    must correspond to the same individual. To mitigate this potential for
    correlation, we recommend that diagnosis key servers pad out diagnosis key
    batches with random keys, with some jitter so exports don't leak the fact
    that they were padded. This is implemented in our reference server
    ([code](https://github.com/google/exposure-notifications-server/blob/821531b167d794ec4a57075cd2009adbcd137505/internal/export/worker.go#L358)).
*   The natural order in which diagnosis keys are stored in the diagnosis
    server's database may confer some information about their association at
    time of upload. To ensure this information is not leaked in diagnosis key
    export batches, we recommend that batches are re-ordered before
    publication, as we've done in our
    [reference implementation](https://github.com/google/exposure-notifications-server/blob/1aef0fbe22aef93ec86f5432dd9d0317418eba15/internal/export/exportfile.go#L139).

### Feedback

If you have questions or feedback about this document, please let us know by
emailing [ens-privsec@google.com](mailto:ens-privsec@google.com).
/build
/*
 * Copyright 2020 Google LLC
 *
 * 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 org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.library'
apply plugin: 'com.google.protobuf'

/*
 * This library has dependencies upon a few other pieces of code, most notably NanoPB and
 * BoringSSL. In order to compile this project, you must run the following bash scripts:
 * $ bash third_party/nanopb/download.sh
 * $ bash third_party/boringssl/build-android.sh
 */
def nanoPbRoot
if (Os.isFamily(Os.FAMILY_MAC)) {
    nanoPbRoot = file("${rootDir}/third_party/nanopb/macosx").absolutePath
} else if (Os.isFamily(Os.FAMILY_UNIX)) {
    nanoPbRoot = file("${rootDir}/third_party/nanopb/linux").absolutePath
} else if (Os.isFamily(Os.FAMILY_WINDOWS)) {
    nanoPbRoot = file("${rootDir}/third_party/nanopb/windows").absolutePath
}
def boringSslRoot = file("${rootDir}/third_party/boringssl/").absolutePath

android {
    compileSdkVersion 29
    buildToolsVersion "30.0.0"
    ndkVersion "21.3.6528147"

    compileOptions {
        // Flag to enable support for the new language APIs
        coreLibraryDesugaringEnabled true
        // Sets Java compatibility to Java 8
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"

        /*
         * - BORING_SSL_ROOT: location of the BoringSSL project root from
         *     https://boringssl.googlesource.com/boringssl/
         * - NANOPB_ROOT: location of NanoPB downloaded from
         *     https://jpa.kapsi.fi/nanopb/download/
         */
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14"
                arguments "-DBORING_SSL_ROOT=${boringSslRoot}", "-DNANOPB_ROOT=${nanoPbRoot}"
            }
        }
        ndk {
            // Explicitly set the supported ABI flavours
            abiFilters "armeabi-v7a", "arm64-v8a", "x86"
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }

    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
}

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.11.4'
    }
    dependencies {
        protobuf files("${nanoPbRoot}/generator/proto/")
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option "lite"
                }
            }
        }
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])

    // Java 8 backports
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'

    // AutoValue Dependencies
    api "com.google.auto.value:auto-value-annotations:1.7.3"
    annotationProcessor "com.google.auto.value:auto-value:1.7.3"

    // Protobuf dependency
    implementation 'com.google.protobuf:protobuf-javalite:3.11.4'

    // Guava dependency
    api "com.google.guava:guava:29.0-android"

    // JODA
    api 'joda-time:joda-time:2.9.4'

    // Play Core library, so we don't have to reimplement the basics such as Task
    implementation 'com.google.android.play:core:1.8.0'

    api "androidx.core:core:1.3.1"
    implementation "androidx.annotation:annotation:1.1.0"

    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'org.robolectric:robolectric:4.3.1'
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
<!--
  ~ Copyright 2020 Google LLC
  ~
  ~ 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.
  -->

<manifest package="com.google.samples.matching">

</manifest>
# Copyright 2020 Google LLC
#
# 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.
#

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# The Exposure Notification API depends on BoringSSL for cryptographic operations.
add_library(crypto STATIC IMPORTED)
add_library(ssl STATIC IMPORTED)

# Configure the location of BoringSSL's libraries
set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${BORING_SSL_ROOT}/${ANDROID_ABI}/crypto/libcrypto.a)
set_target_properties(ssl PROPERTIES IMPORTED_LOCATION ${BORING_SSL_ROOT}/${ANDROID_ABI}/ssl/libssl.a)
include_directories(matching ${BORING_SSL_ROOT}/src/include)

# Determine the correct binary by platform
if(WIN32)
    set(PROTOC "protoc.exe")
else()
    set(PROTOC "protoc")
endif()

# Generate the NanoPB C source and headers
file(MAKE_DIRECTORY ${PROJECT_SOURCE_DIR}/gen)
execute_process(
        COMMAND ${NANOPB_ROOT}/generator-bin/${PROTOC} -I ${NANOPB_ROOT} -I ${NANOPB_ROOT}/generator/proto/ -I ../proto --nanopb_out=./gen ../proto/exposure_key_export.proto
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        OUTPUT_VARIABLE PROTOC_OUTPUT
        RESULT_VARIABLE PROTOC_RESULT
)
# The generated code includes a header file, so add it to the include path
include_directories(matching ${PROJECT_SOURCE_DIR}/gen)

# NanoPB has some include files we'll need.
include_directories(matching ${NANOPB_ROOT})

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

include_directories(matching ${CMAKE_LIBRARY_PATH}/protos)

add_library(matching SHARED

        # NanoPB code
        # We just compile it ourselves since it's very small and simple
        ${NANOPB_ROOT}/pb_encode.c
        ${NANOPB_ROOT}/pb_decode.c
        ${NANOPB_ROOT}/pb_common.c

        # Protobuf Generated Code
        gen/exposure_key_export.pb.c

        # Key matching source
        key_file_parser.cc
        matching_helper.cc
        matchingjni.cc
        nanopb_encoder.cc
        prefix_id_map.cc)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library(log-lib log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries(matching ${log-lib} crypto ssl)
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_CONSTANTS_H_
#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_CONSTANTS_H_

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef __ANDROID__

#include <android/log.h>

#define LOG_TAG "ExposureNotificationJni"
#define LOG_V(...) \
  __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOG_I(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOG_W(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
#define LOG_E(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#else
#include <cstdio>
#define LOG_V(...) fprintf(stderr, __VA_ARGS__)
#define LOG_I(...) fprintf(stderr, __VA_ARGS__)
#define LOG_W(...) fprintf(stderr, __VA_ARGS__)
#define LOG_E(...) fprintf(stderr, __VA_ARGS__)
#endif
namespace exposure {
    constexpr static const int kRpikLength = 16;
    constexpr static const int kTekLength = 16;
    constexpr static const int kIdLength = 16;
    constexpr static const int kIdPerKey = 144;
    constexpr static const int kHkdfInfoLength = 7;
    constexpr static const char kHkdfInfo[] = u8"EN-RPIK";
    constexpr static const int kRpiPaddedDataLength = 12;
    constexpr static const char kRpiPaddedData[] = u8"EN-RPI\0\0\0\0\0\0";

}  // namespace exposure

#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_CONSTANTS_H_
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#include "key_file_parser.h"

namespace exposure {
    bool VerifyHeader(pb_istream_t *pb_istream) {
      char header[kFileHeaderSize] = {0};
      pb_read(pb_istream, reinterpret_cast<pb_byte_t *>(header), kFileHeaderSize);
      int result = memcmp(header, kFileHeader, kFileHeaderSize);
      if (result != 0) {
        LOG_E("Key file header mismatch %s, expected %s", header, kFileHeader);
        return false;
      }
      return true;
    }

    std::unique_ptr<KeyFileIterator> CreateKeyFileIterator(
        const std::string &key_file) {
      FILE *file = fopen(key_file.c_str(), "rb");
      if (file == nullptr) {
        LOG_E("Failed to open file %s", key_file.c_str());
        return nullptr;
      }

      auto buffer = std::make_unique<char[]>(kDefaultBufferSize);
      setbuf(file, buffer.get());

      auto pb_istream = CreatePbInputStream(file);
      if (!VerifyHeader(&pb_istream)) {
        LOG_E("Failed to verify the file header %s", key_file.c_str());
        fclose(file);
        return nullptr;
      }

      LOG_I("Created iterator for %s", key_file.c_str());
      return std::make_unique<KeyFileIterator>(file, std::move(buffer), pb_istream);
    }

    void KeyFileIterator::ReadUntilNextKeyTagOrEnd() {
      pb_wire_type_t wire_type;
      bool eof = false;
      while (!eof) {
        pb_decode_tag(&pb_istream_, &wire_type, &next_tag_, &eof);
        if (IsTagForKeys(next_tag_)) {
          break;
        }
        pb_skip_field(&pb_istream_, wire_type);
      }
    }

    std::unique_ptr<TemporaryExposureKeyNano> KeyFileIterator::ReadNextKey() {
      if (!IsTagForKeys(next_tag_)) {
        LOG_E("Unexpected proto buffer field");
        return nullptr;
      }

      TemporaryExposureKeyNano key = TemporaryExposureKeyNano_init_default;
      if (!pb_decode_delimited(&pb_istream_, TemporaryExposureKeyNano_fields,
                               &key)) {
        LOG_E("Failed to decode exposure key");
        return nullptr;
      }

      ReadUntilNextKeyTagOrEnd();
      return std::make_unique<TemporaryExposureKeyNano>(key);
    }

    bool ReadFromFileToStream(pb_istream_t *stream, pb_byte_t *buffer,
                              size_t size) {
      auto file = reinterpret_cast<FILE *>(stream->state);
      size_t count = fread(buffer, 1, size, file);
      if (ferror(file)) {
        LOG_E("Failed to read input file stream");
        return false;
      }
      if (feof(file)) {
        stream->bytes_left = 0;
      }
      return count == size;
    }

    pb_istream_t CreatePbInputStream(FILE *file) {
      pb_istream_t pb_istream;
      pb_istream.callback = &ReadFromFileToStream;
      pb_istream.state = reinterpret_cast<void *>(file);
      pb_istream.bytes_left = std::numeric_limits<size_t>::max();
      return pb_istream;
    }
}  // namespace exposure
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_KEY_FILE_PARSER_H_
#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_KEY_FILE_PARSER_H_

#include <string>
#include <utility>
#include <vector>

#include "gen/exposure_key_export.pb.h"
#include "constants.h"
#include "pb_decode.h"

#define TemporaryExposureKeyNano \
  com_google_samples_exposurenotification_TemporaryExposureKey
#define TemporaryExposureKeyExportNano_keys_tag \
  com_google_samples_exposurenotification_TemporaryExposureKeyExport_keys_tag  // NOLINT
#define TemporaryExposureKeyNano_init_default \
  com_google_samples_exposurenotification_TemporaryExposureKey_init_default  // NOLINT
#define TemporaryExposureKeyNano_fields \
  com_google_samples_exposurenotification_TemporaryExposureKey_fields  // NOLINT

namespace exposure {
    static const char kFileHeader[] = "EK Export v1    ";
    constexpr static const size_t kFileHeaderSize = sizeof(kFileHeader) - 1;
    constexpr static const int kDefaultBufferSize = 64 * 1024;  // 64 KB

    class KeyFileIterator {
    public:
        // The client of KeyFileIterator transfers the responsibility of closing
        // `file` to KeyFileIterator, to avoid double closing.
        explicit KeyFileIterator(FILE *file, std::unique_ptr<char[]> buffer,
                                 pb_istream_t pb_istream)
            : file_(file),
              buffer_(std::move(buffer)),
              pb_istream_(pb_istream),
              next_tag_(0) {
          ReadUntilNextKeyTagOrEnd();
        }

        ~KeyFileIterator() { fclose(file_); }

        inline bool HasNext() const { return next_tag_ != 0; }

        // Gets the next exposure key if HasNext() return true. If HasNext() return
        // false or failed parse the proto message, return nullptr.
        inline std::unique_ptr<TemporaryExposureKeyNano> Next() {
          return ReadNextKey();
        }

    private:
        void ReadUntilNextKeyTagOrEnd();

        std::unique_ptr<TemporaryExposureKeyNano> ReadNextKey();

        static inline bool IsTagForKeys(uint32_t tag) {
          return tag == TemporaryExposureKeyExportNano_keys_tag;
        }

        FILE *file_;
        std::unique_ptr<char[]> buffer_;
        pb_istream_t pb_istream_;
        uint32_t next_tag_;
    };

    std::vector<std::unique_ptr<TemporaryExposureKeyNano>> ParseFileDirectly(
        const std::string &key_file);

    std::vector<std::unique_ptr<TemporaryExposureKeyNano>> ParseKeysDirectly(
        pb_istream_t *pb_istream);

    bool VerifyHeader(pb_istream_t *pb_istream);

    std::unique_ptr<KeyFileIterator> CreateKeyFileIterator(
        const std::string &key_file);

    pb_istream_t CreatePbInputStream(FILE *file);

// NanoPB Callback, reads the specified size in bytes into buffer from the key
// file 'stream->state', and set the internal state of stream accordingly.
    bool ReadFromFileToStream(pb_istream_t *stream, pb_byte_t *buffer,
                              std::size_t size);
}  // namespace exposure

#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_KEY_FILE_PARSER_H_
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#include "matching_helper.h"

#include <openssl/aes.h>
#include <openssl/cipher.h>
#include <openssl/crypto.h>
#include <openssl/digest.h>
#include <openssl/hkdf.h>
#include <stdio.h>

#include <string>
#include <vector>

#include "key_file_parser.h"
#include "nanopb_encoder.h"
#include "prefix_id_map.h"

constexpr char hexmap[] = {'0', '1', '2', '3', '4', '5', '6', '7',
                           '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

std::string hexStr(const unsigned char *data, int len) {
  std::string s(len * 2, ' ');
  for (int i = 0; i < len; ++i) {
    s[2 * i] = hexmap[(data[i] & 0xF0) >> 4];
    s[2 * i + 1] = hexmap[data[i] & 0x0F];
  }
  return s;
}

extern "C" {

namespace exposure {
    MatchingHelper::MatchingHelper(JNIEnv *env, jobjectArray scan_record_ids) {
      EVP_CIPHER_CTX_init(&context);

      prefix_key_map = std::make_unique<PrefixIdMap>(env, scan_record_ids);
      for (int i = 0; i < kIdPerKey; i++) {
        memcpy(&aesInputStorage[i * kIdLength], kRpiPaddedData,
               kRpiPaddedDataLength);
      }
      last_processed_key_count = 0;
    }

    MatchingHelper::~MatchingHelper() { EVP_CIPHER_CTX_cleanup(&context); }

#if _BYTE_ORDER != _LITTLE_ENDIAN
#error "Must use little endian"
#endif

    // PaddedData[0 - 5]: "EN-RPI".getBytes(UTF_8).
    // PaddedData[6 - 11]: 0x000000000000.
    // PaddedData[12 -15]: enIntervalNumber, uint32 little-endian.
    bool MatchingHelper::GenerateIds(const uint8_t *diagnosis_key,
                                     uint32_t rolling_start_number, uint8_t *ids) {
      uint8_t rpi_key[kRpikLength];
      // RPIK <- HKDF(tek, NULL, UTF8("EN-PRIK"), 16).
      if (HKDF(rpi_key, kRpikLength, EVP_sha256(), diagnosis_key, kTekLength,
          /*salt=*/nullptr, /*salt_len=*/0,
               reinterpret_cast<const uint8_t *>(kHkdfInfo), kHkdfInfoLength) != 1) {
        return false;
      }

      if (EVP_EncryptInit_ex(&context, EVP_aes_128_ecb(), /*impl=*/nullptr, rpi_key,
          /*iv=*/nullptr) != 1) {
        return false;
      }

      uint32_t en_interval_number = rolling_start_number;
      for (int index = 0; index < kIdPerKey * kIdLength;
           index += kIdLength, en_interval_number++) {
        *((uint32_t *) (&aesInputStorage[index + 12])) = en_interval_number;
      }

      int out_length;
      return EVP_EncryptUpdate(&context, ids, &out_length, aesInputStorage,
                               kIdPerKey * kIdLength) == 1;
    }

    jobjectArray MatchingHelper::Matching(
        JNIEnv *env, const std::vector<std::string> &key_files) {
      std::vector<std::unique_ptr<TemporaryExposureKeyNano>> matched_keys;
      uint8_t ids[kIdPerKey * kIdLength];
      static_assert(AES_BLOCK_SIZE == kIdLength, "Incorrect kIdLength.");
      last_processed_key_count = 0;
      for (const auto &key_file : key_files) {
        LOG_I("Matching with %s", key_file.c_str());
        std::unique_ptr<KeyFileIterator> key_file_iterator(
            CreateKeyFileIterator(key_file));
        if (key_file_iterator.get() == nullptr) {
          continue;
        }
        while (key_file_iterator->HasNext()) {
          std::unique_ptr<TemporaryExposureKeyNano> key = key_file_iterator->Next();
          if (key.get() == nullptr) {
            continue;
          }
//          LOG_I("TEK: %s - %d, %d", hexStr(key->key_data.bytes, 16).c_str(),
//                key->has_rolling_start_interval_number ? key->rolling_start_interval_number : -1,
//                key->has_rolling_period ? key->rolling_period : -1);
          last_processed_key_count++;
          if (GenerateIds(key->key_data.bytes,
                          static_cast<uint32_t>(key->rolling_start_interval_number),
                          ids)) {
            for (int j = 0; j < kIdPerKey * kIdLength; j += kIdLength) {
              if (prefix_key_map->GetIdIndex(&ids[j]) >= 0) {
                matched_keys.emplace_back(std::move(key));
                break;
              }
            }
          } else {
            LOG_E("GenerateIds failed");
          }
        }
      }

      if (matched_keys.size() == 0) {
        LOG_I("Matching done, total %d keys, no key matches",
              last_processed_key_count);
        return nullptr;
      }

      LOG_I("Matching done, total %d keys, find %d keys match",
            last_processed_key_count, (int) matched_keys.size());

      jobjectArray proto_array = env->NewObjectArray(
          static_cast<jsize>(matched_keys.size()), env->FindClass("[B"), nullptr);
      for (int i = 0; i < matched_keys.size(); i++) {
        auto serialized = EncodeTemporaryExposureKey(matched_keys.at(i).get());
        jbyteArray byte_array =
            env->NewByteArray(static_cast<jsize>(serialized.size()));
        env->SetByteArrayRegion(byte_array, 0,
                                static_cast<jsize>(serialized.size()),
                                reinterpret_cast<const jbyte *>(serialized.c_str()));
        env->SetObjectArrayElement(proto_array, i, byte_array);
        env->DeleteLocalRef(byte_array);
      }
      return proto_array;
    }

    // Converts a Java jbyteArray (encoding a UTF8 string) to a native UTF8 string.
    std::string JbyteArrayToString(JNIEnv *env, jbyteArray input) {
      jint len = env->GetArrayLength(input);
      std::string output;
      output.resize(len);
      env->GetByteArrayRegion(input, 0, len,
                              reinterpret_cast<jbyte *>(&(output)[0]));
      return output;
    }

    jintArray MatchingHelper::MatchingLegacy(JNIEnv *env,
                                             jobjectArray diagnosis_keys,
                                             jintArray rolling_start_numbers,
                                             int key_count) {
      if (key_count > env->GetArrayLength(diagnosis_keys)) {
        LOG_W("Key count not match diagnosis_keys numbers");
        return nullptr;
      }
      if (key_count > env->GetArrayLength(rolling_start_numbers)) {
        LOG_W("Key count not match rollingStartNumbers");
        return nullptr;
      }
      LOG_I("Matching with %d diagnosis key", key_count);
      std::vector<int> match_indexes;
      jint *rolling_start_number_array =
          env->GetIntArrayElements(rolling_start_numbers, 0);
      uint8_t ids[kIdPerKey * kIdLength];
      static_assert(kIdLength == AES_BLOCK_SIZE, "Incorrect kIdLength.");
      uint8_t *key_bytes[kIdLength];
      for (int i = 0; i < key_count; i++) {
        jbyteArray key_array =
            (jbyteArray) env->GetObjectArrayElement(diagnosis_keys, i);
        env->GetByteArrayRegion(key_array, 0, kIdLength, (jbyte *) key_bytes);
        if (GenerateIds(reinterpret_cast<const uint8_t *>(key_bytes),
                        rolling_start_number_array[i], ids)) {
          for (int j = 0; j < kIdPerKey * kIdLength; j += kIdLength) {
            if (prefix_key_map->GetIdIndex(&ids[j]) >= 0) {
              match_indexes.push_back(i);
              break;
            }
          }
        } else {
          LOG_E("GenerateIds failed");
        }
        env->DeleteLocalRef(key_array);
      }
      env->ReleaseIntArrayElements(rolling_start_numbers,
                                   rolling_start_number_array, 0);

      if (match_indexes.size() == 0) {
        LOG_I("Matching done, no key matches");
        return nullptr;
      }

      LOG_I("Matching done, find %d keys match", (int) match_indexes.size());

      jintArray result = env->NewIntArray((jsize) match_indexes.size());
      env->SetIntArrayRegion(result, 0, match_indexes.size(), match_indexes.data());
      return result;
    }

} // namespace exposure
} // extern "C"

/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_MATCHING_HELPER_H_
#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_MATCHING_HELPER_H_

#include <jni.h>
#include <openssl/aes.h>
#include <openssl/cipher.h>
#include <openssl/crypto.h>
#include <openssl/digest.h>
#include <openssl/hkdf.h>

#include <memory>
#include <string>
#include <vector>

#include "constants.h"
#include "prefix_id_map.h"

namespace exposure {
    class MatchingHelper {
    public:
        MatchingHelper(JNIEnv *env, jobjectArray scan_record_ids);

        ~MatchingHelper();

        // Doing the matching, and return matched diagnosis_keys set.
        jobjectArray Matching(JNIEnv *env, const std::vector<std::string> &key_files);

        // Doing the matching, and return int[] for matched diagnosis_keys indexes.
        jintArray MatchingLegacy(JNIEnv *env, jobjectArray diagnosis_keys,
                                 jintArray rolling_start_numbers, int key_count);

        bool GenerateIds(const uint8_t *diagnosis_key, uint32_t rolling_start_number,
                         uint8_t *ids);

        inline jint LastProcessedKeyCount() const { return last_processed_key_count; }

    private:
        std::unique_ptr<PrefixIdMap> prefix_key_map;
        EVP_CIPHER_CTX context;
        uint8_t aesInputStorage[kIdPerKey * kIdLength];
        uint32_t last_processed_key_count;
    };
}  // namespace exposure
#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_MATCHING_HELPER_H_
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <string>
#include <vector>

#include "constants.h"
#include "matching_helper.h"
#include "prefix_id_map.h"

extern "C" {

#define JND_PACKAGE(name) Java_com_google_samples_exposurenotification_matching_##name
#define JND(name) JND_PACKAGE(MatchingJni_##name)

JNIEXPORT jlong JNICALL JND(initNative)(JNIEnv *env, jclass clazz,
                                        jobjectArray scan_id_records) {
  if (scan_id_records == nullptr) {
    LOG_W("Invalid input for initNative, scan records is null");
    return 0;
  }

  if (env->GetArrayLength(scan_id_records) <= 0) {
    LOG_W("Invalid input for initNative, scan records is empty");
    return 0;
  }

  return reinterpret_cast<jlong>(
      new exposure::MatchingHelper(env, scan_id_records));
}

JNIEXPORT jobjectArray JNICALL
JND(matchingNative)(JNIEnv *env, jclass clazz, jlong native_ptr,
                    jobjectArray key_files_jstring) {
  if (native_ptr == 0 || key_files_jstring == nullptr) {
    LOG_W("Invalid input for matchingNative");
    return nullptr;
  }

  int key_file_count = env->GetArrayLength(key_files_jstring);
  LOG_I("matchingNative get %d files", key_file_count);

  std::vector<std::string> key_files;
  for (int i = 0; i < key_file_count; i++) {
    jstring key_file_jstring =
        (jstring) (env->GetObjectArrayElement(key_files_jstring, i));
    const char *key_file_string = env->GetStringUTFChars(key_file_jstring, 0);
    key_files.push_back(std::string(key_file_string));
    env->ReleaseStringUTFChars(key_file_jstring, key_file_string);
    env->DeleteLocalRef(key_file_jstring);
  }

  exposure::MatchingHelper *wrapper =
      reinterpret_cast<exposure::MatchingHelper *>(native_ptr);
  return wrapper->Matching(env, key_files);
}

JNIEXPORT jintArray JNICALL JND(matchingLegacyNative)(
    JNIEnv *env, jclass clazz, jlong native_ptr, jobjectArray diagnosis_keys,
    jintArray interval_numbers, jint key_count) {
  if (native_ptr == 0 || diagnosis_keys == nullptr ||
      interval_numbers == nullptr) {
    LOG_W("Invalid input for matchingNativeLegacy");
    return nullptr;
  }

  LOG_I("matchingNative get %d keys", key_count);

  exposure::MatchingHelper *wrapper =
      reinterpret_cast<exposure::MatchingHelper *>(native_ptr);
  return wrapper->MatchingLegacy(env, diagnosis_keys, interval_numbers,
                                 key_count);
}

JNIEXPORT jint JNICALL JND(lastProcessedKeyCountNative)(JNIEnv *env,
                                                        jclass clazz,
                                                        jlong native_ptr) {
  if (native_ptr == 0) {
    LOG_W("Invalid input for lastProcessedKeyCount");
    return -1;
  }

  exposure::MatchingHelper *wrapper =
      reinterpret_cast<exposure::MatchingHelper *>(native_ptr);
  return wrapper->LastProcessedKeyCount();
}

JNIEXPORT void JNICALL JND(releaseNative)(JNIEnv *env, jclass clazz,
                                          jlong native_ptr) {
  if (native_ptr == 0) {
    LOG_W("Invalid input for releaseNative");
    return;
  }
  exposure::MatchingHelper *wrapper =
      reinterpret_cast<exposure::MatchingHelper *>(native_ptr);
  delete wrapper;
}
} /* extern "C" */
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#include "nanopb_encoder.h"

#include "constants.h"
#include "pb_encode.h"

namespace exposure {
    std::string EncodeTemporaryExposureKey(
        const TemporaryExposureKeyNano *temporary_exposure_key) {
      uint8_t write_buffer[kEncodedBufferSize];
      pb_ostream_t stream =
          pb_ostream_from_buffer(write_buffer, kEncodedBufferSize);
      if (!pb_encode(&stream, TemporaryExposureKeyNano_fields,
                     temporary_exposure_key)) {
        LOG_E("Failed to encode exposure key");
        return nullptr;
      }

      return std::string(reinterpret_cast<char *>(write_buffer),
                         stream.bytes_written);
    }
}  // namespace exposure
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_NANOPB_ENCODER_H_
#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_NANOPB_ENCODER_H_

#include <string>

#include "gen/exposure_key_export.pb.h"
#include "key_file_parser.h"

namespace exposure {
    constexpr static const int kEncodedBufferSize = 64;

    std::string EncodeTemporaryExposureKey(
        const TemporaryExposureKeyNano *temporary_exposure_key);
}  // namespace exposure
#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_NANOPB_ENCODER_H_
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#include "prefix_id_map.h"

#include <jni.h>

#include <algorithm>

namespace exposure {

    namespace {
        uint16_t GetPrefixInner(const uint8_t *id) {
          uint16_t ret;
          memcpy(&ret, id, sizeof(ret));
          return ret;
        }

        bool Compare(const std::string &lhs, const std::string &rhs) {
          assert(lhs.size() >= 2);
          assert(rhs.size() >= 2);
          return GetPrefixInner(reinterpret_cast<const uint8_t *>(lhs.data())) <
                 GetPrefixInner(reinterpret_cast<const uint8_t *>(rhs.data()));
        }
    }  // namespace

    PrefixIdMap::PrefixIdMap(JNIEnv *env, jobjectArray ble_scan_records) {
      memset(prefix_end_index, 0, sizeof(int) * kIdPrefixIndexSize);
      scan_record_size = env->GetArrayLength(ble_scan_records);

      for (int i = 0; i < scan_record_size; i++) {
        jbyteArray single_id =
            (jbyteArray) env->GetObjectArrayElement(ble_scan_records, i);
        jbyte *id_bytes = env->GetByteArrayElements(single_id, 0);
        scan_records.push_back(std::string((char *) id_bytes, env->GetArrayLength(single_id)));
        env->ReleaseByteArrayElements(single_id, id_bytes, 0);
        env->DeleteLocalRef(single_id);
      }
      std::sort(scan_records.begin(), scan_records.end(), Compare);
      int last_prefix = 0;
      for (int i = 0; i < scan_record_size; i++) {
        int prefix =
            GetPrefix(reinterpret_cast<const uint8_t *>(scan_records[i].data()));
        while (last_prefix < prefix) {
          prefix_end_index[last_prefix++] = i;
        }
      }
      while (last_prefix < kIdPrefixIndexSize) {
        prefix_end_index[last_prefix++] = scan_record_size;
      }
      LOG_I("PrefixIdMap load %d scan records", scan_record_size);
    }

    PrefixIdMap::~PrefixIdMap() { scan_records.clear(); }

    int PrefixIdMap::GetIdIndex(const uint8_t *id) {
      int prefix = GetPrefix(id);
      int start_index = (prefix > 0) ? prefix_end_index[prefix - 1] : 0;
      int end_index = prefix_end_index[prefix];
      for (; start_index < end_index; start_index++) {
        if (memcmp(id, scan_records[start_index].data(), exposure::kIdLength) ==
            0) {
          return start_index;
        }
      }
      return -1;
    }

    uint16_t PrefixIdMap::GetPrefix(const uint8_t *id) {
      return GetPrefixInner(id);
    }
}  // namespace exposure
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_PREFIX_ID_MAP_H_
#define LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_PREFIX_ID_MAP_H_

#include <jni.h>
#include <stdio.h>
#include <stdlib.h>

#include <string>
#include <vector>

#include "constants.h"

namespace exposure {
    constexpr static const int kIdPrefixIndexSize = 65536;

    class PrefixIdMap {
    public:
        int prefix_end_index[exposure::kIdPrefixIndexSize];
        std::vector<std::string> scan_records;
        int scan_record_size;

        // scanRecordIds is a byte[][], which contains all scanned ID from database.
        // Due to we'll only try to copy its value to scan_records, so it's not
        // owned by PrefixIdMap after construction.
        PrefixIdMap(JNIEnv *env, jobjectArray ble_scan_records);

        ~PrefixIdMap();

        int GetIdIndex(const uint8_t *id);

        uint16_t GetPrefix(const uint8_t *id);
    };
}  // namespace exposure
#endif  // LOCATION_NEARBY_CPP_EXPOSURENOTIFICATION_JNI_PREFIX_ID_MAP_H_
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples;

import androidx.annotation.Nullable;

import com.google.auto.value.AutoValue;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * Wrapper class providing value-semantics for {@code byte[]} fields, for example in @{@link
 * AutoValue} classes.
 *
 * <p>The behaviours which give value semantics are:
 *
 * <ul>
 *   <li>The constructor makes a copy of the input(s), to prevent mutation by the caller.
 *   <li>The getter returns a copy of the array, to prevent mutation by the receiver.
 *   <li>{@link #equals} is overridden to compare array content
 *   <li>{@link #hashCode} is overridden to return the hashCode of the array content.
 * </ul>
 * <p>
 * In some cases, the {@code clone()} is unnecessary overhead. Where it really matters, there is a
 * constructor which can take ownership of the {@code byte[]}.
 */
public class ByteArrayValue {
    private final byte[] bytes;

    /**
     * The length of the array.
     */
    public final int length;

    public ByteArrayValue(byte[] bytes) {
        this(bytes, /*takeOwnership=*/ false);
    }

    /**
     * Constructor used for cases where the ByteArrayValue takes ownership of the {@code byte[]},
     * avoiding the need to clone.
     */
    public ByteArrayValue(byte[] bytes, boolean takeOwnership) {
        this.bytes = takeOwnership ? bytes : bytes.clone();
        this.length = this.bytes.length;
    }

    public byte[] get() {
        return bytes.clone();
    }

    /**
     * Direct access to the internal {@code byte[]}. Use in derived classes for read-only access, to
     * avoid the overhead of cloning the array.
     *
     * <p>WARNING: This breaks the encapsulation and could allow the value to be mutated. Only use in
     * performance sensitive cases where the cost of cloning is too high, and the use of the {@code
     * byte[]} is guaranteed to be read-only.
     */
    protected byte[] exposeInternalBytesAndRiskMutation() {
        return bytes;
    }

    /**
     * Compares the content of the array.
     */
    @Override
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof ByteArrayValue)) {
            return false;
        }
        ByteArrayValue that = (ByteArrayValue) other;
        return Arrays.equals(bytes, that.bytes);
    }

    /**
     * Returns a hashCode for the content of the array.
     */
    @Override
    public int hashCode() {
        return Arrays.hashCode(bytes);
    }

    @Override
    public String toString() {
        return new BigInteger(1, bytes).toString(16);
    }

    /**
     * Construct a {@link ByteArrayValue} from a possibly null array.
     *
     * <p>If {@code bytes} is null, return null.
     */
    @Nullable
    public static ByteArrayValue from(@Nullable byte[] bytes) {
        return (bytes != null) ? new ByteArrayValue(bytes) : null;
    }

    /**
     * Construct a value from the concatenation of 1 or more {@code byte[]} inputs.
     */
    public static ByteArrayValue concat(byte[]... arrays) {
        byte[] bytes = concatBytes(arrays);
        return new ByteArrayValue(bytes, /*takeOwnership=*/ true);
    }

    /**
     * Puts the data from this {@link ByteArrayValue} into a {@link ByteBuffer}.
     *
     * @param byteBuffer the destination buffer to {@link ByteBuffer#put} the byte representation.
     */
    public void putIn(ByteBuffer byteBuffer) {
        checkArgument(byteBuffer.remaining() >= length);
        byteBuffer.put(bytes);
    }

    /**
     * Returns the values from each provided array combined into a single array. For example, {@code
     * concat(new byte[] {a, b}, new byte[] {}, new byte[] {c}} returns the array {@code {a, b, c}}.
     *
     * @param arrays zero or more {@code byte} arrays
     * @return a single array containing all the values from the source arrays, in order
     */
    private static byte[] concatBytes(byte[]... arrays) {
        int length = 0;
        for (byte[] array : arrays) {
            length += array.length;
        }
        byte[] result = new byte[length];
        int pos = 0;
        for (byte[] array : arrays) {
            System.arraycopy(array, 0, result, pos, array.length);
            pos += array.length;
        }
        return result;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples;

import android.os.SystemClock;

/**
 * Wrapper interface for time operations. Allows replacement of clock operations for testing.
 */
public interface Clock {

    /**
     * Get the current time of the clock in milliseconds.
     *
     * @return Current time in milliseconds.
     */
    long currentTimeMillis();

    /**
     * Returns milliseconds since boot, including time spent in sleep.
     *
     * @return Current time since boot in milliseconds.
     */
    long elapsedRealtime();

    /**
     * Returns the current timestamp of the most precise timer available on the local system, in
     * nanoseconds.
     *
     * @return Current time in nanoseconds.
     */
    long nanoTime();

    /**
     * Returns the time spent in the current thread, in milliseconds
     *
     * @return Thread time in milliseconds.
     */
    @SuppressWarnings("StaticOrDefaultInterfaceMethod")
    default long currentThreadTimeMillis() {
        return SystemClock.currentThreadTimeMillis();
    }

    public static class DefaultClock implements Clock {

        private static final DefaultClock instance = new DefaultClock();

        public static Clock getInstance() {
            return instance;
        }

        @Override
        public long currentTimeMillis() {
            return System.currentTimeMillis();
        }

        @Override
        public long elapsedRealtime() {
            return SystemClock.elapsedRealtime();
        }

        @Override
        public long nanoTime() {
            return System.nanoTime();
        }

        @Override
        public long currentThreadTimeMillis() {
            return SystemClock.currentThreadTimeMillis();
        }

        private DefaultClock() {
        }
    }

}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples;

/**
 * Utilities related to hexadecimal byte representation.
 */
public class Hex {

    private static final char[] HEX_UPPERCASE = {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
    };
    private static final char[] HEX_LOWERCASE = {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    };

    /**
     * @see {http://stackoverflow.com/a/9855338/3447400}
     */
    public static String bytesToStringUppercase(byte[] bytes) {
        return bytesToStringUppercase(bytes, false /* zeroTerminated */);
    }

    /**
     * Encodes a byte array as a hexadecimal representation of bytes.
     */
    public static String bytesToStringUppercase(byte[] bytes, boolean zeroTerminated) {
        int length = bytes.length;
        StringBuilder out = new StringBuilder(length * 2);
        for (int i = 0; i < length; i++) {
            if (zeroTerminated && i == length - 1 && (bytes[i] & 0xff) == 0) {
                break;
            }
            out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
            out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
        }
        return out.toString();
    }

    /**
     * @see {http://stackoverflow.com/a/9855338/3447400}
     */
    public static String bytesToStringLowercase(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        int j = 0;
        for (int i = 0; i < bytes.length; i++) {
            int v = bytes[i] & 0xFF;
            hexChars[j++] = HEX_LOWERCASE[v >>> 4];
            hexChars[j++] = HEX_LOWERCASE[v & 0x0F];
        }
        return new String(hexChars);
    }

    public static String bytesToColonDelimitedStringUppercase(byte[] bytes) {
        if (bytes.length == 0) {
            return new String();
        }
        char[] hexChars = new char[bytes.length * 3 - 1];
        int j = 0;
        for (int i = 0; i < bytes.length - 1; i++) {
            int v = bytes[i] & 0xFF;
            hexChars[j++] = HEX_UPPERCASE[v >>> 4];
            hexChars[j++] = HEX_UPPERCASE[v & 0x0F];
            hexChars[j++] = ':';
        }
        int v = bytes[bytes.length - 1] & 0xFF;
        hexChars[j++] = HEX_UPPERCASE[v >>> 4];
        hexChars[j++] = HEX_UPPERCASE[v & 0x0F];
        return new String(hexChars);
    }

    public static String bytesToColonDelimitedStringLowercase(byte[] bytes) {
        if (bytes.length == 0) {
            return new String();
        }
        char[] hexChars = new char[bytes.length * 3 - 1];
        int j = 0;
        for (int i = 0; i < bytes.length - 1; i++) {
            int v = bytes[i] & 0xFF;
            hexChars[j++] = HEX_LOWERCASE[v >>> 4];
            hexChars[j++] = HEX_LOWERCASE[v & 0x0F];
            hexChars[j++] = ':';
        }
        int v = bytes[bytes.length - 1] & 0xFF;
        hexChars[j++] = HEX_LOWERCASE[v >>> 4];
        hexChars[j++] = HEX_LOWERCASE[v & 0x0F];
        return new String(hexChars);
    }

    public static byte[] colonDelimitedStringToBytes(String hex) {
        return stringToBytes(hex.replace(":", ""));
    }

    public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
        int length = hex.length();
        if (length % 2 != 0) {
            throw new IllegalArgumentException("Hex string has odd number of characters");
        }
        byte[] out = new byte[length / 2];
        for (int i = 0; i < length; i += 2) {
            // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
            // our hex values are in 0, 255.
            out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
        }
        return out;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples;

import android.os.Build;

public class PlatformVersion {
    public static boolean isAtLeastM() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
    }

    public static boolean isAtLeastO() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * Utility class to manage logging of messages with a similar API to systems in Google Play
 * Services.
 */
public class Log {
    private static final String TAG = "ExposureNotification";
    public static final Log log = new Log();

    public Logger atVerbose() {
        return new Logger(android.util.Log::v);
    }

    public Logger atFine() {
        return new Logger(android.util.Log::d);
    }

    public Logger atDebug() {
        return new Logger(android.util.Log::d);
    }

    public Logger atInfo() {
        return new Logger(android.util.Log::i);
    }

    public Logger atWarning() {
        return new Logger(android.util.Log::w);
    }

    public Logger atSevere() {
        return new Logger(android.util.Log::e);
    }

    private Log() {
    }

    public static final class Logger {
        @NonNull
        private final LogFunction<String, String, Throwable> out;
        @Nullable
        private Throwable cause;

        Logger(@NonNull LogFunction<String, String, Throwable> out) {
            this.out = out;
        }

        public Logger withCause(@NonNull Throwable cause) {
            this.cause = cause;
            return this;
        }

        public void log(@NonNull String message) {
            out.invoke(TAG, message, cause);
        }

        public void log(@NonNull String format, Object... args) {
            log(String.format(format, args));
        }
    }

    private interface LogFunction<P1, P2, P3> {
        void invoke(P1 p1, P2 p2, P3 p3);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification;

import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType;
import com.google.samples.exposurenotification.storage.TekMetadata;

/**
 * Helpers for working with TEK metadata.
 */
public final class TekMetadataUtils {

    /**
     * Returns true if the metadata's report type is supported by clients. This excludes revoked, for
     * example.
     */
    public static boolean isTekReportTypeReturnableToClients(TekMetadata metadata) {
        return metadata.getReportType() == ReportType.CONFIRMED_TEST
                || metadata.getReportType() == ReportType.CONFIRMED_CLINICAL_DIAGNOSIS
                || metadata.getReportType() == ReportType.SELF_REPORT
                || metadata.getReportType() == ReportType.RECURSIVE;
    }

    private TekMetadataUtils() {
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.annotations;

/**
 * This interface is used only to preserve compatibility with the annotations used in the Exposure
 * Notifications code, but the actual implementation is not available here because it's not relevant
 * to the usage of Exposure Notifications.
 */
public @interface Hide {
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.advertising;

import androidx.annotation.VisibleForTesting;

import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.AdvertisementPacket;
import com.google.samples.exposurenotification.ble.interfaces.BleKeyGenerator;
import com.google.samples.exposurenotification.crypto.CryptoException;
import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
import com.google.samples.exposurenotification.data.BluetoothMetadata;
import com.google.samples.exposurenotification.data.RollingProximityId;
import com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataGenerator;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import static com.google.samples.exposurenotification.data.BluetoothMetadata.CALIBRATION_CONFIDENCE_BITS_MASK;
import static com.google.samples.exposurenotification.data.BluetoothMetadata.CALIBRATION_CONFIDENCE_BITS_OFFSET;
import static com.google.samples.exposurenotification.data.BluetoothMetadata.VERSION_1_0;
import static com.google.samples.exposurenotification.data.BluetoothMetadata.VERSION_1_1;

/**
 * Generates advertisement packet following v1.3 of the BLE spec.
 */
public class BleAdvertisementGenerator implements BleKeyGenerator {
    private final RollingProximityIdManager proxIdManager;

    public BleAdvertisementGenerator(RollingProximityIdManager proxIdManager) {
        this.proxIdManager = proxIdManager;
    }

    @Override
    public AdvertisementPacket generatePacket() throws CryptoException {
        // byte 0 - version
        // byte 1 - transmit power level
        // byte 2 - reserved
        // byte 3 - reserved
        return generatePacket(
                BluetoothMetadata.create(
                        createVersion(),
                        (byte)
                                (ContactTracingFeature.txCalibrationPower()
                                        + ContactTracingFeature.txCalibrationPowerToTxPower())));
    }

    @VisibleForTesting
    AdvertisementPacket generatePacket(BluetoothMetadata metadata) throws CryptoException {
        RollingProximityId rollingProximityId = proxIdManager.getCurrentRollingProximityId();
        Log.log.atInfo().log("Generating advertisement with version=%s, tx=%s",
                metadata.version(), metadata.txPower());
        // TODO: Consider store AEM generator to avoid recomputation of AEM key for 143 more times.
        AssociatedEncryptedMetadataGenerator aemGenerator =
                new AssociatedEncryptedMetadataGenerator(proxIdManager.getTemporaryExposureKey());
        AssociatedEncryptedMetadata encryptedMetadata =
                aemGenerator.encrypt(rollingProximityId, metadata);

        return new AdvertisementPacket.Builder()
                .setProxId(rollingProximityId.getDirect())
                .setRawMetadata(metadata.toBytes())
                .setEncryptedMetadata(encryptedMetadata.getDirect())
                .build();
    }

    @VisibleForTesting
    static byte createVersion() {
        if (ContactTracingFeature.advertisementMetadataV11()) {
            // bit 7:6 - major version
            // bit 5:4 - minor version
            // bit 3:2 - transmit power calibration confidence
            // bit 1:0 - reserved
            return (byte)
                    (VERSION_1_1
                            | ((ContactTracingFeature.calibrationConfidence()
                            << CALIBRATION_CONFIDENCE_BITS_OFFSET)
                            & CALIBRATION_CONFIDENCE_BITS_MASK));
        } else {
            // Bits 7:6 = major version (01)
            // Bits 5:4 = minor version (00)
            // Bits 3:0 = reserved (0000)
            return VERSION_1_0;
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.advertising;

import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
import android.bluetooth.le.AdvertisingSet;
import android.bluetooth.le.AdvertisingSetCallback;
import android.bluetooth.le.AdvertisingSetParameters;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Consumer;

import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.AdvertisementPacket;
import com.google.samples.exposurenotification.ble.utils.Constants.StatusCode;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import java.util.Objects;

import static com.google.common.base.Verify.verifyNotNull;
import static com.google.samples.exposurenotification.ble.utils.Constants.CONTACT_TRACER_UUID;
import static com.google.samples.exposurenotification.ble.utils.Constants.StatusCode.ERROR_BLUETOOTH_UNKNOWN;
import static com.google.samples.exposurenotification.ble.utils.Constants.StatusCode.OK;

/**
 * BLE advertiser class. Used to advertising personal ID for contact tracing.
 */
public class BleAdvertiser {
    @Nullable
    private PlatformBasedAdvertiser advertiser;

    private boolean started = false;

    public BleAdvertiser() {
    }

    /**
     * Verifies device currently meets all constraints needed to advertise
     * FIXME: Implement checks to ensure the device supports BLE Advertising
     */
    public StatusCode supportsAdvertising() {
        return OK;
    }

    /**
     * Indicates whether the advertiser is running.
     */
    public boolean isAdvertising() {
        return started;
    }

    /**
     * Starts advertising. Subsequent calls to this function cause advertising to restart with the
     * latest packet.
     */
    public StatusCode startAdvertising(AdvertisementPacket packet) {
        /*
         * This is the key to ensuring a new, random Bluetooth MAC address is used when rotating
         * to the next RPI. When advertising is restarted, a new, random Bluetooth MAC address
         * will be used.
         */
        stopAdvertising();

        StatusCode statusCode = supportsAdvertising();
        if (statusCode != OK) {
            Log.log.atWarning().log("Advertising not supported, error code=%s", statusCode.name());
            return statusCode;
        }

        AdvertiseData data =
                new AdvertiseData.Builder()
                        .addServiceUuid(CONTACT_TRACER_UUID)
                        .addServiceData(CONTACT_TRACER_UUID, packet.getPacket())
                        .build();
        PlatformBasedAdvertiser advertiserLocal = advertiser;
        if (advertiserLocal == null) {
            advertiserLocal = advertiser = PlatformBasedAdvertiser.getInstance(this::onStartFailure);
        }

        started = (advertiserLocal != null && advertiserLocal.advertise(data));
        Log.log.atInfo().log("Start advertising, started=%b", started);
        return started ? OK : ERROR_BLUETOOTH_UNKNOWN;
    }

    /**
     * Stops advertising. Should only be called when user opts out, or contact tracing is no longer
     * necessary.
     */
    public StatusCode stopAdvertising() {
        StatusCode statusCode = OK;
        if (started) {
            // startAdvertising() will ensure that advertiser is non-null
            // noinspection ConstantConditions
            boolean stopResult = verifyNotNull(advertiser).stopAdvertising();
            statusCode = stopResult ? OK : ERROR_BLUETOOTH_UNKNOWN;
            Log.log.atInfo().log("stopAdvertising get stopResult=%b", stopResult);
        }
        resetState();
        return statusCode;
    }

    /**
     * Ensures any internal state is reset if advertising support is lost at any point (BLE turns off)
     * or if advertising is stopped.
     */
    private void resetState() {
        started = false;
    }

    private void onStartFailure(int errorCode) {
        resetState();
        Log.log.atInfo().log("Start advertising failed with %s", getErrorCodeString(errorCode));
    }

    private abstract static class PlatformBasedAdvertiser {
        @Nullable
        static PlatformBasedAdvertiser getInstance(Consumer<Integer> onFailCallback) {
            if (VERSION.SDK_INT >= VERSION_CODES.O
                    && ContactTracingFeature.advertiseUseOreoAdvertiser()) {
                return new OreoAdvertiser(onFailCallback);
            } else {
                return new BaseAdvertiser(onFailCallback);
            }
        }

        abstract boolean advertise(AdvertiseData data);

        abstract boolean stopAdvertising();
    }

    /**
     * For pre O devices.
     */
    private static class BaseAdvertiser extends PlatformBasedAdvertiser {
        private final ContactTracerAdvertiseCallback advertiseCallback;

        BaseAdvertiser(Consumer<Integer> onFailCallback) {
            advertiseCallback = new ContactTracerAdvertiseCallback(onFailCallback);
        }

        @Override
        boolean advertise(AdvertiseData data) {
            int advertiseMode = (int) ContactTracingFeature.advertiseMode();
            int txPowerLevel = (int) ContactTracingFeature.advertiseTxPower();
            AdvertiseSettings settings =
                    new AdvertiseSettings.Builder()
                            .setAdvertiseMode(advertiseMode)
                            .setTxPowerLevel(txPowerLevel)
                            .setConnectable(ContactTracingFeature.advertiseConnectable())
                            .build();

            // FIXME: Call through to BluetoothLeAdvertiser#startAdvertising
            boolean started = Objects.nonNull(advertiseCallback);
            if (started) {
                Log.log
                        .atInfo()
                        .log(
                                "Start advertising with packet (%s) mode: %s tx power level %s",
                                data, getAdvertiseModeString(advertiseMode), getTxPowerLevelString(txPowerLevel));
            }

            return started;
        }

        @Override
        boolean stopAdvertising() {
            // FIXME: Call through to BluetoothLeAdvertiser#stopAdvertising
            return Objects.nonNull(advertiseCallback);
        }

        /**
         * Notified after attempting to start advertising of whether the advertisement registration was
         * a success or not.
         */
        private static class ContactTracerAdvertiseCallback extends AdvertiseCallback {
            private final Consumer<Integer> onFailCallback;

            private ContactTracerAdvertiseCallback(Consumer<Integer> onFailCallback) {
                this.onFailCallback = onFailCallback;
            }

            @Override
            public void onStartFailure(int errorCode) {
                onFailCallback.accept(errorCode);
            }

            @Override
            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                Log.log.atInfo().log("Start advertising success");
            }
        }
    }

    /**
     * For O and above devices.
     */
    @RequiresApi(api = VERSION_CODES.O)
    private static class OreoAdvertiser extends PlatformBasedAdvertiser {
        private final ContactTracerAdvertisingSetCallback advertisingSetCallback;

        OreoAdvertiser(Consumer<Integer> onFailCallback) {
            advertisingSetCallback = new ContactTracerAdvertisingSetCallback(onFailCallback);
        }

        @Override
        boolean advertise(AdvertiseData data) {
            boolean legacyMode = ContactTracingFeature.advertiseLegacyMode();
            int advertiseInterval = (int) ContactTracingFeature.advertiseInterval();
            int txPower = (int) ContactTracingFeature.advertiseTxPowerO();
            boolean connectable = ContactTracingFeature.advertiseConnectable();
            boolean scannable = ContactTracingFeature.advertiseScannable();
            AdvertisingSetParameters parameters =
                    new AdvertisingSetParameters.Builder()
                            .setLegacyMode(legacyMode)
                            .setInterval(advertiseInterval)
                            .setTxPowerLevel(txPower)
                            .setConnectable(connectable)
                            .setScannable(scannable)
                            .build();

            // FIXME: Call through to BluetoothLeAdvertiser#startAdvertising
            boolean started = Objects.nonNull(advertisingSetCallback);
            if (started) {
                Log.log
                        .atInfo()
                        .log(
                                "Start advertising with packet (%s) interval: %d, txPower %d,"
                                        + " legacyMode=%b, connectable=%b, scannable=%b",
                                data, advertiseInterval, txPower, legacyMode, connectable, scannable);
            }

            return started;
        }

        @Override
        boolean stopAdvertising() {
            // FIXME: Call through to BluetoothLeAdvertiser#stopAdvertising
            return Objects.nonNull(advertisingSetCallback);
        }

        /**
         * Notified after attempting to start advertising of whether the advertisement registration was
         * a success or not.
         */
        private static class ContactTracerAdvertisingSetCallback extends AdvertisingSetCallback {
            private final Consumer<Integer> onFailCallback;

            private ContactTracerAdvertisingSetCallback(Consumer<Integer> onFailCallback) {
                this.onFailCallback = onFailCallback;
            }

            @Override
            public void onAdvertisingSetStarted(AdvertisingSet set, int txPower, int status) {
                Log.log.atInfo().log("Start advertising,  txPower=%d, status=%d", txPower, status);
                if (status != AdvertisingSetCallback.ADVERTISE_SUCCESS) {
                    onFailCallback.accept(status);
                }
            }
        }
    }

    private static String getErrorCodeString(int errorCode) {
        switch (errorCode) {
            case AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED:
                return "ALREADY_STARTED";
            case AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE:
                return "DATA_TOO_LARGE";
            case AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
                return "FEATURE_UNSUPPORTED";
            case AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR:
                return "INTERNAL_ERROR";
            case AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
                return "TOO_MANY_ADVERTISERS";
            default:
                return "Unknown code " + errorCode;
        }
    }

    private static String getAdvertiseModeString(int advertiseMode) {
        switch (advertiseMode) {
            case AdvertiseSettings.ADVERTISE_MODE_LOW_POWER:
                return "ADVERTISE_MODE_LOW_POWER";
            case AdvertiseSettings.ADVERTISE_MODE_BALANCED:
                return "ADVERTISE_MODE_BALANCED";
            case AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY:
                return "ADVERTISE_MODE_LOW_LATENCY";
            default:
                return "Unknown mode " + advertiseMode;
        }
    }

    private static String getTxPowerLevelString(int txPowerLevel) {
        switch (txPowerLevel) {
            case AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW:
                return "ADVERTISE_TX_POWER_ULTRA_LOW";
            case AdvertiseSettings.ADVERTISE_TX_POWER_LOW:
                return "ADVERTISE_TX_POWER_LOW";
            case AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM:
                return "ADVERTISE_TX_POWER_MEDIUM";
            case AdvertiseSettings.ADVERTISE_TX_POWER_HIGH:
                return "ADVERTISE_TX_POWER_HIGH";
            default:
                return "Unknown level " + txPowerLevel;
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.advertising;

import androidx.annotation.Nullable;
import androidx.core.util.Supplier;

import com.google.common.base.Preconditions;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.ble.interfaces.BleDatabaseWriter;
import com.google.samples.exposurenotification.ble.utils.Constants;
import com.google.samples.exposurenotification.crypto.CryptoException;
import com.google.samples.exposurenotification.data.RollingProximityId;
import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
import com.google.samples.exposurenotification.data.generator.RollingProximityIdGenerator;
import com.google.samples.exposurenotification.data.generator.TemporaryExposureKeyGenerator;
import com.google.samples.exposurenotification.features.ContactTracingFeature;
import com.google.samples.exposurenotification.storage.SelfTemporaryExposureKeyDataStore;

import java.time.Instant;

import static com.google.common.base.Verify.verifyNotNull;
import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * Generate rolling proximity identifiers for BLE advertising, using v1.3.
 */
public class RollingProximityIdManager {
    private final BleDatabaseWriter bleDatabaseWriter;
    private final Supplier<Long> currentTimeMsSupplier;
    @Nullable
    private final SelfTemporaryExposureKeyDataStore selfTemporaryExposureKeyDataStore;
    private int latestEnIntervalNumber;
    @Nullable
    private TemporaryExposureKey temporaryExposureKey;
    @Nullable
    private RollingProximityId latestRollingProximityId;

    public RollingProximityIdManager(
            @Nullable SelfTemporaryExposureKeyDataStore selfTemporaryExposureKeyDataStore,
            BleDatabaseWriter bleDatabaseWriter,
            Supplier<Long> currentTimeMsSupplier) {
        this.selfTemporaryExposureKeyDataStore = selfTemporaryExposureKeyDataStore;
        this.bleDatabaseWriter = bleDatabaseWriter;
        this.currentTimeMsSupplier = currentTimeMsSupplier;
    }

    public RollingProximityIdManager(
            @Nullable SelfTemporaryExposureKeyDataStore selfTemporaryExposureKeyDataStore,
            BleDatabaseWriter bleDatabaseWriter,
            Supplier<Long> currentTimeMsSupplier,
            TemporaryExposureKey temporaryExposureKey) {
        this.selfTemporaryExposureKeyDataStore = selfTemporaryExposureKeyDataStore;
        this.bleDatabaseWriter = bleDatabaseWriter;
        this.currentTimeMsSupplier = currentTimeMsSupplier;
        this.temporaryExposureKey = temporaryExposureKey;
    }

    /**
     * Gets the rolling proximity id for current time (given instant).
     */
    public synchronized RollingProximityId getCurrentRollingProximityId() throws CryptoException {
        Instant instant = Instant.ofEpochMilli(currentTimeMsSupplier.get());
        int currentEnIntervalNumber = getCurrentEnIntervalNumber(instant);
        if (latestRollingProximityId != null && currentEnIntervalNumber == latestEnIntervalNumber) {
            Log.log
                    .atInfo()
                    .log(
                            "getCurrentRollingProximityId: same latestEnIntervalNumber=%d",
                            latestEnIntervalNumber);
            return verifyNotNull(latestRollingProximityId);
        }

        Log.log
                .atInfo()
                .log(
                        "getCurrentRollingProximityId: current/latestEnIntervalNumber=%d/%d",
                        currentEnIntervalNumber, latestEnIntervalNumber);
        //
        if (temporaryExposureKey == null && selfTemporaryExposureKeyDataStore != null) {
            TemporaryExposureKey existingKey =
                    selfTemporaryExposureKeyDataStore.getSingleKeyForIntervalNumber(currentEnIntervalNumber);
            // Keep the nullness checker happy, by never assigning null to temporaryExposureKey.
            if (existingKey != null) {
                temporaryExposureKey = existingKey;
            }
        }
        if (temporaryExposureKey == null
                || currentEnIntervalNumber
                >= TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey)) {
            temporaryExposureKey =
                    TemporaryExposureKeyGenerator.generateKey(
                            getAlignedEnIntervalNumber(currentEnIntervalNumber));
            Log.log.atInfo().log("getCurrentRollingProximityId: generated a new Temporary Exposure Key");
            Log.log
                    .atVerbose()
                    .log(
                            "getCurrentRollingProximityId: TEK=%s",
                            Constants.fromIdByteArrayToString(verifyNotNull(temporaryExposureKey).getKeyData()));
            bleDatabaseWriter.writeNewTemporaryExposureKey(verifyNotNull(temporaryExposureKey));
        }

        verifyNotNull(temporaryExposureKey);

        // Generates a new Rolling Proximity Id.
        latestEnIntervalNumber = currentEnIntervalNumber;
        latestRollingProximityId =
                new RollingProximityIdGenerator(
                        temporaryExposureKey.getKeyData(),
                        temporaryExposureKey.getRollingStartIntervalNumber(),
                        TemporaryExposureKeySupport.getRollingEndIntervalNumber(temporaryExposureKey),
                        (int) ContactTracingFeature.rollingProximityIdKeySizeBytes(),
                        ContactTracingFeature.rpikHkdfInfoString(),
                        ContactTracingFeature.rpidAesPaddedString())
                        .generateId(latestEnIntervalNumber);
        Log.log
                .atInfo()
                .log(
                        "getCurrentRollingProximityId: generated a new RollingProximityId=%s",
                        latestRollingProximityId);
        return verifyNotNull(latestRollingProximityId);
    }

    synchronized TemporaryExposureKey getTemporaryExposureKey() {
        // getCurrentRollingProximityId() must always be called before this method
        Preconditions.checkNotNull(temporaryExposureKey, "Must call getCurrentRollingProximityId()");
        return temporaryExposureKey;
    }

    private static int getCurrentEnIntervalNumber(Instant instant) {
        // TODO: Make sure using ceil or floor.
        return (int) (instant.toEpochMilli() /
                MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes()));
    }

    private static int getAlignedEnIntervalNumber(int enIntervalNumber) {
        int tkRollingPeriod = (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
        return tkRollingPeriod * (enIntervalNumber / tkRollingPeriod);
    }

    public synchronized void clearCache() {
        Log.log.atInfo().log("RollingProximityIdManager.clearCache");
        latestEnIntervalNumber = 0;
        temporaryExposureKey = null;
        latestRollingProximityId = null;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.data;

import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.data.GeneratedRollingProximityId;
import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import org.joda.time.Duration;

/**
 * Utility method to validate whether a sighting falls within a valid time window.
 *
 * <p>A valid sighting falls within {@link
 * ContactTracingFeature#tkMatchingClockDriftRollingPeriods()} of the start and end of a {@link
 * GeneratedRollingProximityId#intervalNumber()}
 */
public class RollingProximityIdValidator {

    /** Returns true if the sighting is within the expected time bounds. */
    public static boolean isSightingValid(
            GeneratedRollingProximityId generatedId,
            Duration sightingTimeSinceEpoch,
            TemporaryExposureKey diagnosisKey) {
        int sightingTimeIntervalNumber =
                TimeIntervalNumberUtility.getTimeIntervalNumber(sightingTimeSinceEpoch);
        return sightingTimeIntervalNumber >= getValidWindowStartIntervalNumber(generatedId)
                && sightingTimeIntervalNumber < getValidWindowEndIntervalNumber(generatedId, diagnosisKey);
    }

    /**
     * Gets the start of the valid time window based on {@link
     * GeneratedRollingProximityId#intervalNumber()}.
     */
    public static int getValidWindowStartIntervalNumber(GeneratedRollingProximityId generatedId) {
        return (int)
                (generatedId.intervalNumber() - ContactTracingFeature.tkMatchingClockDriftRollingPeriods());
    }

    /**
     * Gets the exclusive end of the valid time window based on {@link
     * GeneratedRollingProximityId#intervalNumber()}. Clock drift will only be considered if the
     * resulting validEndIntervalNumber falls within the provided {@code diagnosisKey}'s validity
     * period. If the computed validStartIntervalNumber results in an ending interval later than the
     * provided {@code diagnosisKey}, then the {@code diagnosisKey}'s end interval is returned
     * instead.
     */
    public static int getValidWindowEndIntervalNumber(
            GeneratedRollingProximityId generatedId, TemporaryExposureKey diagnosisKey) {
        int windowEndIntervalNumber =
                (int)
                        (generatedId.intervalNumber()
                                + ContactTracingFeature.tkMatchingClockDriftRollingPeriods()
                                + 1);
        int keyEndIntervalNumber =
                TemporaryExposureKeySupport.getRollingEndIntervalNumber(diagnosisKey);
        if (ContactTracingFeature.ignoreEmbargoPeriodWhenMatchingNearKeyEdges()
                && windowEndIntervalNumber > keyEndIntervalNumber) {
            windowEndIntervalNumber = keyEndIntervalNumber;
        }
        return windowEndIntervalNumber;
    }
    private RollingProximityIdValidator() {}
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.data;

import com.google.samples.Clock.DefaultClock;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import org.joda.time.Duration;

import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * A utility class for methods used to convert time interval numbers to time and vice versa.
 */
public class TimeIntervalNumberUtility {
    /**
     * Converts the current time to a time interval number. Using DefaultClock for testability
     */
    public static int getCurrentIntervalNumber() {
        return (int)
                (DefaultClock.getInstance().currentTimeMillis()
                        / MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes()));
    }

    /**
     * Converts the passed in time since epoch into a time interval number, based on the feature flag
     * values that are provided.
     */
    public static int getTimeIntervalNumber(Duration timeSinceEpoch) {
        return (int)
                (timeSinceEpoch.getStandardMinutes() / ContactTracingFeature.idRollingPeriodMinutes());
    }

    /**
     * Gets the latest {@link TemporaryExposureKey#getRollingStartIntervalNumber()}} from before the
     * provided time.
     */
    public static int getLatestStartIntervalNumber(Duration timeSinceEpoch) {
        int numKeysSinceEpoch =
                (int)
                        (numIntervalsPer(timeSinceEpoch)
                                / ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
        return numKeysSinceEpoch
                * (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
    }

    /**
     * Returns the number of intervals passed wtihin a given {@code duration}.
     */
    public static int numIntervalsPer(Duration duration) {
        return (int) (duration.getStandardMinutes() / ContactTracingFeature.idRollingPeriodMinutes());
    }

    /**
     * Calculates milliseconds since epoch from {@code intervalNumber}.
     */
    public static long getMillisSinceEpoch(int intervalNumber) {
        return Duration.standardMinutes(intervalNumber * ContactTracingFeature.idRollingPeriodMinutes())
                .getMillis();
    }

    private TimeIntervalNumberUtility() {
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.interfaces;

import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;

/**
 * Stores BLE scanning and advertising history to database.
 */
public interface BleDatabaseWriter {
    /**
     * Writes sighting contact tracing ID into database.
     */
    void writeBleSighting(
            byte[] rollingProximityId,
            int rssi,
            byte[] associatedEncryptedMetadata,
            int previousScanEpochSeconds);

    /**
     * Writes new generated contact tracing ID into database.
     */
    void writeNewTemporaryExposureKey(TemporaryExposureKey temporaryExposureKey);
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.interfaces;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Provides executor for BLE scanning and advertising.
 */
public interface BleExecutors {
    /**
     * Schedules a {@code command} with specific delay.
     *
     * @return the canceller of this scheduled task
     */
    ScheduledTask schedule(Runnable command, long delay, TimeUnit unit);

    /**
     * Schedules a {@code command} with specific delay with {@link ScheduledExecutorService}.
     *
     * @return the canceller of this scheduled task
     */
    ScheduledTask scheduleWithScheduledExecutor(Runnable command, long delay, TimeUnit unit);

    /**
     * Schedules a {@code command} with specific delay. OpportunisticSchedule will not be executed
     * exactly on time, but will be checked everytime when {@link BleExecutors#schedule(Runnable,
     * long, TimeUnit)} called or its scheduled task fired. If current time is larger than target time
     * when checking, the opportunisticSchedule will be executed.
     */
    ScheduledTask opportunisticSchedule(Runnable runnable, long delay, TimeUnit unit);

    void start();

    void stop();

    /**
     * Allows scheduled runnable to be cancelled, and also provides status check for cancellation.
     */
    interface ScheduledTask {
        boolean isCancelable();

        boolean cancel();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.interfaces;

import com.google.samples.exposurenotification.nearby.AdvertisementPacket;
import com.google.samples.exposurenotification.crypto.CryptoException;

/**
 * Interface used to generate rotating proximity ID for BLE advertisment.
 */
public interface BleKeyGenerator {

    /**
     * Generates a new AdvertisementPacket according to
     * https://covid19-static.cdn-apple.com/applications/covid19/current/static/contact-tracing/pdf/ExposureNotification-CryptographySpecificationv1.1.pdf
     */
    AdvertisementPacket generatePacket() throws CryptoException;
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.interfaces;

import com.google.samples.exposurenotification.ble.utils.Constants.StatusCode;

/**
 * BLE scanner interface. Used to scan nearby BLE advertisements for contact tracing.
 */
public interface BleScanner {
    /**
     * Checks if current device is able to scan.
     */
    StatusCode supportScanning();

    /**
     * Starts advertising. Should only be called when user opt in, or reboot completed.
     */
    StatusCode startScanning();

    /**
     * Starts advertising. Should only be called when user opt out, or contact tracing is no longer
     * necessary.
     */
    StatusCode stopScanning();

    /**
     * Indicates current scanner's state is scanning or not.
     */
    boolean isScanning();
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.scanner;

import android.annotation.TargetApi;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanFilter.Builder;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.ContentResolver;
import android.content.Context;
import android.media.AudioManager;
import android.os.Build.VERSION_CODES;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.WorkSource;
import android.provider.Settings;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.arch.core.util.Function;
import androidx.core.util.Supplier;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.samples.PlatformVersion;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.ble.interfaces.BleDatabaseWriter;
import com.google.samples.exposurenotification.ble.interfaces.BleExecutors;
import com.google.samples.exposurenotification.ble.interfaces.BleExecutors.ScheduledTask;
import com.google.samples.exposurenotification.ble.interfaces.BleScanner;
import com.google.samples.exposurenotification.ble.utils.BluetoothUtils;
import com.google.samples.exposurenotification.ble.utils.Constants;
import com.google.samples.exposurenotification.ble.utils.Constants.StatusCode;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;

import static com.google.samples.exposurenotification.ble.utils.Constants.StatusCode.ERROR_BLUETOOTH_UNKNOWN;
import static com.google.samples.exposurenotification.ble.utils.Constants.StatusCode.OK;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Implementation of {@link BleScanner}, used to scan BLE advertisement for contact tracing.
 */
@TargetApi(VERSION_CODES.LOLLIPOP)
@SuppressWarnings("MissingPermission")
public class BleScannerImpl implements BleScanner {
    private static final String WAKE_LOCK_TAG = "nearby:ExposureNotificationScanner";
    private static final int MAX_RSSI_VALUE = 126;
    private static final int MIN_RSSI_VALUE = -127;
    private static final ImmutableList<Integer> CHECKING_PROFILES =
            ImmutableList.of(BluetoothProfile.A2DP, BluetoothProfile.HEADSET);
    private static final int SCAN_CHANNEL_NUMBER = 3;

    private final Context context;
    private final BleExecutors bleExecutors;
    private final Supplier<Long> currentTimeMsSupplier;
    private final BleSightingCallback bleSightingCallback;
    private final AudioManager audioManager;
    private final PowerManager powerManager;
    @VisibleForTesting
    final ScanStatusManager scanStatusManager;
    private final ScanTimePersistence scanTimePersistence;

    @Nullable
    @VisibleForTesting
    ScheduledTask scheduledTask;
    @Nullable
    private ScheduledTask piggybackScheduledTask;
    private long lastScheduleTimeMsForStartScan;
    /**
     * Master WakeLock that keeps the cpu awake while scanning.
     */
    private final WakeLock wakeLock;

    @Nullable
    private WorkSource workSource;

    private Function<Integer, Integer> randomFunction =
            integer -> new SecureRandom().nextInt(integer);
    private Predicate<List<Integer>> profileInUsingPredicator =
            BluetoothUtils::isBluetoothProfileInUsing;

    private int defaultScanWindowMs;
    private int defaultScanIntervalMs;

    // This object will be fully initialized before rescheduleScanForOpportunisticScan() is called
    // incompatible types in assignment.
    public BleScannerImpl(
            Context context,
            BleDatabaseWriter databaseWriter,
            BleExecutors bleExecutors,
            Supplier<Long> currentTimeMsSupplier,
            Runnable startScanFailCallback) {
        this.context = context;
        this.bleExecutors = bleExecutors;
        this.currentTimeMsSupplier = currentTimeMsSupplier;
        this.scanTimePersistence = new ScanTimePersistence();
        this.scanStatusManager = new ScanStatusManager();
        this.bleSightingCallback =
                new BleSightingCallback(
                        databaseWriter,
                        () -> rescheduleScanForOpportunisticScan(/* ignoreFrequency= */ false),
                        scanStatusManager,
                        scanTimePersistence,
                        startScanFailCallback);
        this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        this.powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        // FIXME: Aquire a WakeLock
        this.wakeLock = null;
        this.defaultScanIntervalMs = getCurrentSettingsGlobalScanInterval();
        this.defaultScanWindowMs = getCurrentSettingsGlobalScanWindow();
    }

    /**
     * Verifies the device supports BLE scanning.
     * FIXME: Implement checks to ensure the device supports BLE Advertising
     */
    @Override
    public StatusCode supportScanning() {
        return OK;
    }

    @Override
    public boolean isScanning() {
        return !scanStatusManager.isStopped();
    }

    @VisibleForTesting
    @Nullable
    WorkSource getWorkSource() {
        return workSource;
    }

    @Override
    public StatusCode startScanning() {
        StatusCode statusCode = supportScanning();
        if (statusCode != OK) {
            return statusCode;
        }

        if (isScanning()) {
            return OK;
        }

        setActiveClientAsWorkSource();
        return startScanningInternal(/* isPiggyBackScan= */ false) ? OK : ERROR_BLUETOOTH_UNKNOWN;
    }

    private void setActiveClientAsWorkSource() {
        if (workSource != null) {
            // FIXME: Remove old WorkSource if needed
            workSource = null;
        }

        // FIXME: Get record of calling package
        String callingPackage = "com.google.android.apps.exposurenotification";
        // FIXME: Create a meaningful WorkSource
        workSource = new WorkSource();
        if (workSource != null) {
            wakeLock.setWorkSource(workSource);
            Log.log.atInfo().log("BleScanner set work source to %s", callingPackage);
        }
    }

    private synchronized boolean startScanningInternal(boolean isPiggyBackScan) {
        if (scanStatusManager.isScanning()) {
            Log.log.atInfo().log("Already scanning!");
            return false;
        }
        cancelAllScheduleTasks();

        maybeUpdateBleScannerInstance();

        scanStatusManager.setScanning();
        int scanMode = (int) ContactTracingFeature.scanMode();
        ScanSettings.Builder builder = new ScanSettings.Builder();
        builder.setScanMode(scanMode);
        if (PlatformVersion.isAtLeastM()) {
            int callbackType = ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
            int matchMode = ScanSettings.MATCH_MODE_STICKY;
            builder.setCallbackType(callbackType).setMatchMode(matchMode);
            if (PlatformVersion.isAtLeastO()) {
                builder.setLegacy(ContactTracingFeature.scanOnlyLegacy());
                Log.log
                        .atInfo()
                        .log("Set scanning parameters. setLegacy=%b", ContactTracingFeature.scanOnlyLegacy());
            }
            Log.log
                    .atInfo()
                    .log("Set scanning parameters. matchMode=%d, callback type=%d", matchMode, callbackType);
        }

        int durationSeconds = getScanTime(isPiggyBackScan);

        // Set new scanning parameters allowing us to more closely control which channels we're using.
        // The channel is only updated when the window value passes, meaning that with the default
        // implementation we will scan for 4s on channel 37, then 4s on channel 38, and 4s on channel
        // 39 before returning to 37. By shrinking the window, we can cause scanning to cycle through
        // all 3 channels over the course of our EN scan.
        defaultScanWindowMs = getCurrentSettingsGlobalScanWindow();
        defaultScanIntervalMs = getCurrentSettingsGlobalScanInterval();
        int scanChannelTimeMs = (durationSeconds * 1000) / SCAN_CHANNEL_NUMBER;
        setScanningSettingsGlobalParameters(scanChannelTimeMs, scanChannelTimeMs);

        ScanFilter filter = new Builder()
                .setServiceData(Constants.CONTACT_TRACER_UUID, new byte[]{0}, new byte[]{0})
                .build();
        // FIXME: Call through to BluetoothLeScanner#startScan with the `filter` declared above
        //  and the bleSightingCallback callback.
        boolean result = true;
        Log.log.atInfo().log("Starting scanning. scanMode=%d", scanMode);
        if (durationSeconds > 0) {
            Log.log.atInfo().log("Schedule stop the scan after %d seconds", durationSeconds);
            scheduledTask =
                    bleExecutors.schedule(
                            () -> {
                                if (PlatformVersion.isAtLeastM()
                                        && ContactTracingFeature.enableOpportunisticScan()) {
                                    stopScanningInternal();
                                    scanStatusManager.setPaused();
                                    startOpportunisticScanningInternal();
                                } else {
                                    pauseScanning();
                                }
                            },
                            durationSeconds,
                            SECONDS);
            if (ContactTracingFeature.wakelockForScan()) {
                Log.log.atInfo().log("Hold wakelock %d seconds for scanning", durationSeconds + 1);
                wakeLock.acquire(SECONDS.toMillis(durationSeconds + 1));
            }
        }
        return result;
    }

    @RequiresApi(VERSION_CODES.M)
    private synchronized boolean startOpportunisticScanningInternal() {
        if (scanStatusManager.isOpportunisticScanning()) {
            Log.log.atInfo().log("Already opportunistic scanning!");
            return false;
        }

        maybeUpdateBleScannerInstance();

        scanStatusManager.setOpportunisticScanning();
        ScanSettings.Builder builder =
                new ScanSettings.Builder()
                        .setScanMode(ScanSettings.SCAN_MODE_OPPORTUNISTIC)
                        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                        .setMatchMode(ScanSettings.MATCH_MODE_STICKY);
        if (PlatformVersion.isAtLeastO()) {
            builder.setLegacy(ContactTracingFeature.scanOnlyLegacy());
        }

        ScanFilter filter = new Builder()
                .setServiceData(Constants.CONTACT_TRACER_UUID, new byte[]{0}, new byte[]{0})
                .build();
        // FIXME: Call through to BluetoothLeScanner#startScan with the `filter` declared above
        //  and the bleSightingCallback callback.
        boolean result = true;
        Log.log.atInfo().log("Starting opportunistic scanning.");
        rescheduleScanForOpportunisticScan(/* ignoreFrequency= */ true);
        return result;
    }

    private void rescheduleScanForOpportunisticScan(boolean ignoreFrequency) {
        if (!scanStatusManager.isOpportunisticScanning()) {
            return;
        }
        if (!ignoreFrequency) {
            long timeMsSinceLastSchedule = currentTimeMsSupplier.get() - lastScheduleTimeMsForStartScan;
            if (timeMsSinceLastSchedule < ContactTracingFeature.rescheduleStartScanMinIntervalMs()) {
                Log.log
                        .atInfo()
                        .log(
                                "Reschedule interval too short, timeMsSinceLastSchedule=%d",
                                timeMsSinceLastSchedule);
                return;
            }
        }

        rescheduleStartScan();
        lastScheduleTimeMsForStartScan = currentTimeMsSupplier.get();
    }

    private void rescheduleStartScan() {
        cancelAllScheduleTasks();

        int intervalSeconds = getScanInterval();
        Log.log.atInfo().log("Schedule start normal scan after %d seconds", intervalSeconds);
        scheduledTask =
                bleExecutors.schedule(
                        () -> restartScanByScheduledTask(/* isPiggyBackScan= */ false),
                        intervalSeconds,
                        SECONDS);
        schedulePiggybackScan();
    }

    private synchronized void schedulePiggybackScan() {
        if (!ContactTracingFeature.enablePiggybackScan()) {
            return;
        }

        long startTime = currentTimeMsSupplier.get();
        int piggyBackScanMinIntervalSeconds =
                (int) ContactTracingFeature.piggybackScanIntervalSeconds();
        int piggybackScanDelaySeconds =
                (PlatformVersion.isAtLeastM() && powerManager.isDeviceIdleMode())
                        ? (int) ContactTracingFeature.piggybackScanDelayWhileDeviceIdleSeconds()
                        : powerManager.isInteractive()
                        ? piggyBackScanMinIntervalSeconds
                        : (int) ContactTracingFeature.piggybackScanDelayWhileScreenOffSeconds();
        Log.log.atInfo().log("Try to piggyback scan after %d seconds", piggybackScanDelaySeconds);
        piggybackScheduledTask =
                bleExecutors.scheduleWithScheduledExecutor(
                        () -> {
                            int actualDelay =
                                    (int) MILLISECONDS.toSeconds(currentTimeMsSupplier.get() - startTime);
                            if (actualDelay - piggyBackScanMinIntervalSeconds
                                    < ContactTracingFeature.piggybackAccetableMinDelaySeconds()) {
                                Log.log
                                        .atInfo()
                                        .log(
                                                "Ignored piggyback task, actually delayed %ds is too short, cpu might"
                                                        + " still awake",
                                                actualDelay);
                                schedulePiggybackScan();
                                return;
                            }

                            Log.log.atInfo().log("Executing piggyback task, actually delayed %ds", actualDelay);
                            restartScanByScheduledTask(/* isPiggyBackScan= */ true);
                        },
                        piggybackScanDelaySeconds,
                        SECONDS);
    }

    private synchronized void restartScanByScheduledTask(boolean isPiggyBackScan) {
        if (scanStatusManager.isScanning()) {
            Log.log.atInfo().log("Already scanning, skip restartScanByScheduledTask");
            return;
        }
        Log.log.atInfo().log("restartScanByScheduledTask prepare to restart scan");
        stopScanningInternal();
        scanStatusManager.setPaused();
        startScanningInternal(isPiggyBackScan);
    }

    private synchronized void cancelAllScheduleTasks() {
        if (scheduledTask != null) {
            scheduledTask.cancel();
            scheduledTask = null;
        }
        if (piggybackScheduledTask != null) {
            piggybackScheduledTask.cancel();
            piggybackScheduledTask = null;
        }
    }

    @Override
    public StatusCode stopScanning() {
        Log.log.atInfo().log("User turn off scanning.");
        cancelAllScheduleTasks();

        boolean result = stopScanningInternal();
        if (scanStatusManager.isPaused()) {
            result = true;
        }
        scanStatusManager.setStopped();
        return result ? OK : ERROR_BLUETOOTH_UNKNOWN;
    }

    private synchronized void pauseScanning() {
        stopScanningInternal();
        scanStatusManager.setPaused();
        rescheduleStartScan();
    }

    private synchronized boolean stopScanningInternal() {
        try {
            if (!scanStatusManager.isScanning() && !scanStatusManager.isOpportunisticScanning()) {
                Log.log.atInfo().log("Not scanning!");
                return false;
            }

            Log.log.atInfo().log("Stopping scanning.");
            scanTimePersistence.markLastScanStopTime();

            // Reset the scanning parameters back to their default values. This is so that other apps also
            // performing scanning aren't affected long term by the updated values set during
            // startScanningInternal.
            setScanningSettingsGlobalParameters(defaultScanWindowMs, defaultScanIntervalMs);

            // FIXME: Call BluetoothLeScanner#stopScan with bleSightingCallback
            return true;
        } finally {
            if (wakeLock.isHeld()) {
                wakeLock.release();
            }
        }
    }

    private void maybeUpdateBleScannerInstance() {
        // FIXME: Get a reference to a BluetoothLeScanner if necessary
    }

    private int getScanTime(boolean isPiggyBackScan) {
        if (ContactTracingFeature.scanIntervalSeconds() <= 0) {
            return 0;
        }

        int scanTime =
                (int)
                        (isPiggyBackScan
                                ? ContactTracingFeature.piggybackScanTimeSeconds()
                                : ContactTracingFeature.scanTimeSeconds());

        if (needToExtendScanTime()) {
            Log.log.atInfo().log("Extend the scan time because bluetooth is in using.");
            return (int) (scanTime + ContactTracingFeature.scanTimeExtendForProfileInUseSeconds());
        }
        return scanTime;
    }

    private boolean needToExtendScanTime() {
        if (ContactTracingFeature.extendScanOnlyForBtAudioOrCalls()) {
            if (audioManager.isBluetoothScoOn()) {
                Log.log.atInfo().log("Using bluetooth device for communication");
                return true;
            } else if (audioManager.isMusicActive() && audioManager.isBluetoothA2dpOn()) {
                Log.log.atInfo().log("Using bluetooth device for streaming audio");
                return true;
            }
            return false;
        }

        if (profileInUsingPredicator.apply(CHECKING_PROFILES)) {
            Log.log.atInfo().log("Extend the san time because some bt profiles is in using.");
            return true;
        } else {
            return false;
        }
    }

    @VisibleForTesting
    void setProfileInUsingPredicator(Predicate<List<Integer>> profileInUsingPredicator) {
        this.profileInUsingPredicator = profileInUsingPredicator;
    }

    private int getScanInterval() {
        if (ContactTracingFeature.scanIntervalSeconds() <= 0) {
            return 1;
        }

        int scanIntervalSeconds = (int) ContactTracingFeature.scanIntervalSeconds();
        int scanRandomRange = (int) ContactTracingFeature.scanIntervalRandomRangeSeconds();

        if (scanRandomRange >= scanIntervalSeconds) {
            scanRandomRange = scanIntervalSeconds - 1;
        }

        scanIntervalSeconds =
                scanIntervalSeconds - (scanRandomRange > 0 ? randomFunction.apply(scanRandomRange) : 0);

        if (powerManager.isInteractive()) {
            scanIntervalSeconds =
                    (int) (scanIntervalSeconds * ContactTracingFeature.scanIntervalRatioWhenScreenOn());
            Log.log
                    .atInfo()
                    .log(
                            "BleScanner adjust scan interval due to screen on, adjust ratio %.2f",
                            ContactTracingFeature.scanIntervalRatioWhenScreenOn());
        }

        if (audioManager.isMusicActive()) {
            scanIntervalSeconds =
                    (int) (scanIntervalSeconds * ContactTracingFeature.scanIntervalRatioWhenAudioPlayback());
            Log.log
                    .atInfo()
                    .log(
                            "BleScanner adjust scan interval due to audio playback, adjust ratio %.2f",
                            ContactTracingFeature.scanIntervalRatioWhenAudioPlayback());
        }

        int minInterval =
                Math.min(
                        (int) ContactTracingFeature.minScanIntervalSeconds(),
                        (int) ContactTracingFeature.scanIntervalSeconds());
        if (scanIntervalSeconds < minInterval) {
            scanIntervalSeconds = minInterval;
        }
        return scanIntervalSeconds;
    }

    @VisibleForTesting
    int getCurrentSettingsGlobalScanWindow() {
        return getCurrentSettingsGlobalScanParameter(
                ContactTracingFeature.lowLatencyScanWindowSettingsName(),
                (int) ContactTracingFeature.lowLatencyScanWindowDefaultValueMillis());
    }

    @VisibleForTesting
    int getCurrentSettingsGlobalScanInterval() {
        return getCurrentSettingsGlobalScanParameter(
                ContactTracingFeature.lowLatencyScanIntervalSettingsName(),
                (int) ContactTracingFeature.lowLatencyScanIntervalDefaultValueMillis());
    }

    private int getCurrentSettingsGlobalScanParameter(String name, int defaultValue) {
        return Settings.Global.getInt(context.getContentResolver(), name, defaultValue);
    }

    private void setScanningSettingsGlobalParameters(int scanWindow, int scanInterval) {
        if (ContactTracingFeature.bleScanningAdjustIntervalAndWindowValues()) {
            ContentResolver resolver = context.getContentResolver();
            Settings.Global.putInt(
                    resolver, ContactTracingFeature.lowLatencyScanWindowSettingsName(), scanWindow);
            Settings.Global.putInt(
                    resolver, ContactTracingFeature.lowLatencyScanIntervalSettingsName(), scanInterval);
        }
    }

    @VisibleForTesting
    void setRandomFunction(Function<Integer, Integer> randomFunction) {
        this.randomFunction = randomFunction;
    }

    private static class BleSightingCallback extends ScanCallback {
        private final BleDatabaseWriter databaseWriter;
        private final Runnable rescheduler;
        private final ScanTimePersistence scanTimePersistence;
        private final ScanStatusManager scanStatusManager;
        private final Runnable startScanFailCallback;

        public BleSightingCallback(
                BleDatabaseWriter databaseWriter,
                Runnable rescheduler,
                ScanStatusManager scanStatusManager,
                ScanTimePersistence scanTimePersistence,
                Runnable startScanFailCallback) {
            this.databaseWriter = databaseWriter;
            this.rescheduler = rescheduler;
            this.scanStatusManager = scanStatusManager;
            this.scanTimePersistence = scanTimePersistence;
            this.startScanFailCallback = startScanFailCallback;
        }

        @Override
        public void onScanResult(int callbackType, ScanResult scanResult) {
            ScanRecord scanRecord;
            if (scanResult == null
                    || (scanRecord = scanResult.getScanRecord()) == null
                    || scanRecord.getServiceData() == null) {
                Log.log.atWarning().log("Empty scanResult or getScanRecord, skipping.");
                return;
            }
            byte[] serviceData = scanRecord.getServiceData(Constants.CONTACT_TRACER_UUID);
            if (serviceData == null || serviceData.length <= 0) {
                Log.log.atWarning().log("Empty service data, skipping.");
                return;
            }

            if (serviceData.length < ContactTracingFeature.contactIdLength()) {
                Log.log.atWarning().log("Invalid ID:%s", Constants.fromIdByteArrayToString(serviceData));
                return;
            }

            byte[] idByte = Arrays.copyOf(serviceData, (int) ContactTracingFeature.contactIdLength());
            String id = Constants.fromIdByteArrayToString(idByte);
            byte[] metadata =
                    serviceData.length > ContactTracingFeature.contactIdLength()
                            ? Arrays.copyOfRange(
                            serviceData, (int) ContactTracingFeature.contactIdLength(), serviceData.length)
                            : new byte[0];
            int adjustedRssi = getAdjustedRssi(scanResult);
            // Default value, used if we have not seen any previous scans.
            int previousScanEpochSeconds = 0;
            // Opportunistic scans can only measure time since last stopScanning(), not from each other.
            if (scanStatusManager.isOpportunisticScanning()) {
                scanTimePersistence.markLastOpportunisticSightingTime();
                previousScanEpochSeconds = scanTimePersistence.getLastScanStopTime();
            } else {
                // Scheduled sightings measure time since last stopScanning() or since the last
                // opportunistic sighting, whichever is later.
                previousScanEpochSeconds =
                        Math.max(
                                scanTimePersistence.getLastScanStopTime(),
                                scanTimePersistence.getLastOpportunisticSightingTime());
            }
            Log.log
                    .atInfo()
                    // LINT.IfChange
                    .log(
                            "Scan device %s, type=%d, id=%s, raw_rssi=%d, calibrated_rssi=%d, meta=%s,"
                                    + " previous_scan=%d",
                            scanResult.getDevice(),
                            callbackType,
                            id,
                            scanResult.getRssi(),
                            adjustedRssi,
                            metadata.length <= 0 ? "Empty" : Constants.fromIdByteArrayToString(metadata),
                            previousScanEpochSeconds);
            // ID is used only for deduping.
            rescheduler.run();
            databaseWriter.writeBleSighting(idByte, adjustedRssi, metadata, previousScanEpochSeconds);
        }

        @Override
        public void onScanFailed(int errorCode) {
            Log.log.atWarning().log("Scan failed! errorCode=%d", errorCode);
            if (errorCode == SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) {
                startScanFailCallback.run();
            }
        }

        private static int getAdjustedRssi(ScanResult scanResult) {
            int adjustedRssi = scanResult.getRssi() + (int) ContactTracingFeature.rssiOffset();
            if (adjustedRssi > MAX_RSSI_VALUE) {
                adjustedRssi = MAX_RSSI_VALUE;
            } else if (adjustedRssi < MIN_RSSI_VALUE) {
                adjustedRssi = MIN_RSSI_VALUE;
            }
            return adjustedRssi;
        }
    }

    @VisibleForTesting
    static class ScanStatusManager {
        enum Status {
            SCANNING,
            OPPORTUNISTIC_SCANNING,
            PAUSED,
            USER_STOPPED_SCANNING
        }

        private Status status = Status.USER_STOPPED_SCANNING;

        public ScanStatusManager() {
        }

        public Status getStatus() {
            return status;
        }

        public boolean isStopped() {
            return status == Status.USER_STOPPED_SCANNING;
        }

        public boolean isScanning() {
            return status == Status.SCANNING;
        }

        public boolean isOpportunisticScanning() {
            return status == Status.OPPORTUNISTIC_SCANNING;
        }

        public boolean isPaused() {
            return status == Status.PAUSED;
        }

        public void setScanning() {
            setStatus(Status.SCANNING);
        }

        public void setPaused() {
            setStatus(Status.PAUSED);
        }

        public void setOpportunisticScanning() {
            setStatus(Status.OPPORTUNISTIC_SCANNING);
        }

        public void setStopped() {
            setStatus(Status.USER_STOPPED_SCANNING);
        }

        private void setStatus(Status status) {
            this.status = status;
        }
    }

    private class ScanTimePersistence {
        private int lastScanStopTimeSeconds = 0;
        private int lastOpportunisticSightingTimeSeconds = 0;

        public ScanTimePersistence() {
        }

        public void markLastScanStopTime() {
            lastScanStopTimeSeconds = (int) MILLISECONDS.toSeconds(currentTimeMsSupplier.get());
        }

        public void markLastOpportunisticSightingTime() {
            lastOpportunisticSightingTimeSeconds =
                    (int) MILLISECONDS.toSeconds(currentTimeMsSupplier.get());
        }

        public int getLastScanStopTime() {
            return lastScanStopTimeSeconds;
        }

        public int getLastOpportunisticSightingTime() {
            return lastOpportunisticSightingTimeSeconds;
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.utils;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;

import androidx.core.content.ContextCompat;

import java.util.List;

/**
 * Utility class for bluetooth.
 */
@SuppressWarnings("MissingPermission")
public class BluetoothUtils {
    public static boolean isBluetoothProfileInUsing(List<Integer> profiles) {
        /**
         * FIXME: Get the device's Bluetooth Adapter from
         * {@link ContextCompat#getSystemService(Context, Class)} with the class
         * {@link BluetoothManager}
         */
        BluetoothAdapter adapter = null;
        if (adapter == null || !adapter.isEnabled()) {
            return false;
        }

        for (int profile : profiles) {
            int connectingState = adapter.getProfileConnectionState(profile);
            if (connectingState == BluetoothAdapter.STATE_CONNECTED
                    || connectingState == BluetoothAdapter.STATE_CONNECTING) {
                return true;
            }
        }
        return false;
    }

    private BluetoothUtils() {
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.ble.utils;

import static com.google.common.io.BaseEncoding.base16;

import android.os.ParcelUuid;

import com.google.samples.exposurenotification.nearby.ExposureNotificationStatusCodes;

import java.util.UUID;

/** Constants for BLE scanner and advertiser. */
public class Constants {
    public static final int METADATA_LENGTH = 4;
    public static final ParcelUuid CONTACT_TRACER_UUID = new ParcelUuid(to128BitUuid((short) 0xFD6F));
    private static final int BIT_INDEX_OF_16_BIT_UUID = 32;

    public static UUID to128BitUuid(short shortUuid) {
        UUID baseUuid = UUID.fromString("00000000-0000-1000-8000-00805F9B34FB");
        return new UUID(
                ((shortUuid & 0xFFFFL) << BIT_INDEX_OF_16_BIT_UUID) | baseUuid.getMostSignificantBits(),
                baseUuid.getLeastSignificantBits());
    }

    public static String fromIdByteArrayToString(byte[] idByteArray) {
        return base16().encode(idByteArray);
    }

    /** The result of start or stop scanning & advertising. */
    public enum StatusCode {
        UNKNOWN(ExposureNotificationStatusCodes.FAILED),
        OK(ExposureNotificationStatusCodes.SUCCESS),
        ERROR_BLUETOOTH_NOT_SUPPORTED(ExposureNotificationStatusCodes.FAILED_NOT_SUPPORTED),
        ERROR_BLUETOOTH_DISABLED(ExposureNotificationStatusCodes.FAILED_BLUETOOTH_DISABLED),
        ERROR_BLUETOOTH_UNKNOWN(ExposureNotificationStatusCodes.INTERNAL_ERROR);

        private final int apiStatusCode;

        StatusCode(int apiStatusCode) {
            this.apiStatusCode = apiStatusCode;
        }

        public int getApiStatusCode() {
            return apiStatusCode;
        }
    }

    private Constants() {}
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.crypto;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * AesCtrEncryptor helper functions.
 */
public class AesCtrEncryptor {
    private AesCtrEncryptor() {
    }

    public static byte[] aesCtr(byte[] key, byte[] iv, byte[] data) throws CryptoException {
        try {
            Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));

            return cipher.doFinal(data);
        } catch (NoSuchPaddingException
                | NoSuchAlgorithmException
                | InvalidAlgorithmParameterException
                | InvalidKeyException
                | BadPaddingException
                | IllegalBlockSizeException e) {
            throw new CryptoException(e);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.crypto;

import android.annotation.SuppressLint;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.SecretKeySpec;

/**
 * Encryptor that encapsultes aes/ecb encryption, but separates initialization and encryption steps.
 *
 * <p>If used repeatedly, call {@link #create()} and {@link #init(byte[])} sparingly.
 */
public final class AesEcbEncryptor {

    private final Cipher cipher;

    @SuppressLint("GetInstance")
    public static AesEcbEncryptor create() throws CryptoException {
        try {
            return new AesEcbEncryptor(Cipher.getInstance("AES/ECB/NoPadding"));
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            throw new CryptoException(e);
        }
    }

    private AesEcbEncryptor(Cipher cipher) {
        this.cipher = cipher;
    }

    /**
     * Initializes encryption with provided key.
     */
    public void init(byte[] key) throws CryptoException {
        try {
            cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"));
        } catch (InvalidKeyException e) {
            throw new CryptoException(e);
        }
    }

    /**
     * Encrypts provided data.
     */
    public byte[] encrypt(byte[] data) throws CryptoException {
        try {
            return cipher.doFinal(data);
        } catch (BadPaddingException | IllegalBlockSizeException e) {
            throw new CryptoException(e);
        }
    }

    /**
     * Encrypts provided data to a given output buffer.
     */
    public byte[] encrypt(byte[] data, byte[] output) throws CryptoException {
        try {
            cipher.doFinal(data, /*inputOffset =*/ 0, data.length, output, /* outputOffset =*/ 0);
            return output;
        } catch (BadPaddingException | IllegalBlockSizeException | ShortBufferException e) {
            throw new CryptoException(e);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.crypto;

/**
 * Exception related to cryptographic operations in exposure notification.
 */
@SuppressWarnings("unused")
public class CryptoException extends Exception {
    public CryptoException() {
        super();
    }

    public CryptoException(Throwable throwable) {
        super(throwable);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.crypto;

import androidx.annotation.Nullable;

import com.google.common.base.Preconditions;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * Key derivation helper functions.
 */
public final class KeyDerivation {
    private static final int HKDF_OUTPUT_LENGTH = 16;
    private static final int HASH_LENGTH = 32;
    /**
     * The algorithm name used for Exposure Notification Cryptography.
     */
    public static final String ALGORITHM_NAME = "HmacSHA256";

    private KeyDerivation() {
    }

    /**
     * Note that this function is for Exposure Notification Cryptography Specification 1.1 only, it
     * only support 16-byte length output.
     */
    public static byte[] hkdfSha256(
            byte[] inputKeyingMaterial, @Nullable byte[] inputSalt, byte[] info, int length)
            throws CryptoException {
        try {
            Mac mac = Mac.getInstance(ALGORITHM_NAME);
            return hkdfSha256(mac, inputKeyingMaterial, inputSalt, info, length);
        } catch (NoSuchAlgorithmException e) {
            throw new CryptoException(e);
        }
    }

    /**
     * Note that this function is for Exposure Notification Cryptography Specification 1.1 only, it
     * only support 16-byte length output.
     */
    public static byte[] hkdfSha256(
            Mac mac, byte[] inputKeyingMaterial, @Nullable byte[] inputSalt, byte[] info, int length)
            throws CryptoException {
        Preconditions.checkArgument(mac.getAlgorithm().equals(ALGORITHM_NAME));
        if (length != HKDF_OUTPUT_LENGTH) {
            throw new CryptoException(new NoSuchAlgorithmException("Only support 16-byte."));
        }
        byte[] salt = (inputSalt == null || inputSalt.length == 0) ? new byte[HASH_LENGTH] : inputSalt;
        try {
            return hkdfSha256ExpandLength16(mac, hkdfSha256Extract(mac, inputKeyingMaterial, salt), info);
        } catch (InvalidKeyException e) {
            throw new CryptoException(e);
        }
    }

    /**
     * The HKDF (RFC 5869) extraction function, using the SHA-256 hash function. The output PRK is
     * calculated as follows: PRK = HMAC-SHA256(salt, IKM), i.e. salt as the key of hmac-sha256, and
     * ikm (input keying material) as the message.
     */
    private static byte[] hkdfSha256Extract(Mac mac, byte[] inputKeyingMaterial, byte[] salt)
            throws InvalidKeyException {
        mac.init(new SecretKeySpec(salt, ALGORITHM_NAME));

        return mac.doFinal(inputKeyingMaterial);
    }

    /**
     * HKDF (RFC 5869) expansion function, using the SHA-256 hash function, the output size is
     * 16-byte.
     */
    private static byte[] hkdfSha256ExpandLength16(Mac mac, byte[] pseudoRandomKey, byte[] info)
            throws InvalidKeyException {
        // For length being 16 cases, counter always equals to 0x01.
        byte[] counter = {0x01};
        mac.init(new SecretKeySpec(pseudoRandomKey, ALGORITHM_NAME));
        mac.update(info);

        return Arrays.copyOf(mac.doFinal(counter), HKDF_OUTPUT_LENGTH);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data;

import com.google.samples.ByteArrayValue;

/**
 * The Associated Encrypted Metadata is data encrypted along with the Rolling Proximity Identifier
 * and that can only be decrypted later, if the user broadcasting it is tested positive and reveals
 * their Temporary Exposure Key.
 */
public final class AssociatedEncryptedMetadata extends ByteArrayValue {

    /**
     * Create an {@link AssociatedEncryptedMetadata} by taking ownership of a {@code byte[]}.
     *
     * <p>WARNING: This value could be unexpectedly mutated if {@code data} will still be accessible
     * to the caller. To ensure an immutable value, use {@code new AssociatedEncryptedMetadata(data,
     * false)} to construct from a copy of {@code data}.
     */
    public static AssociatedEncryptedMetadata create(byte[] data) {
        return new AssociatedEncryptedMetadata(data, /*takeOwnership=*/ true);
    }

    public AssociatedEncryptedMetadata(byte[] data, boolean takeOwnership) {
        super(data, takeOwnership);
    }

    /**
     * Return a direct reference to the underlying data.
     *
     * <p>WARNING: This breaks the encapsulation and could allow the value to be mutated. Only use in
     * performance sensitive cases where the cost of cloning is too high, and the use of the {@code
     * byte[]} is guaranteed to be read-only.
     */
    public byte[] getDirect() {
        return exposeInternalBytesAndRiskMutation();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data;

import com.google.auto.value.AutoValue;
import com.google.samples.exposurenotification.ExposureNotificationEnums.CalibrationConfidence;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * The structured metadata sent and received via Bluetooth protocol. The actual payload that get
 * transmitted over Bluetooth is an encrypted copy of this class. See {@link
 * AssociatedEncryptedMetadata} and {@link
 * com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataGenerator}.
 */
@AutoValue
public abstract class BluetoothMetadata {
    public static final byte VERSION_1_0 = 0x40;
    public static final byte VERSION_1_1 = 0x50;

    public static final int CALIBRATION_CONFIDENCE_BITS_OFFSET = 2;
    public static final byte CALIBRATION_CONFIDENCE_BITS_MASK = (byte) 0b00001100;

    public static BluetoothMetadata create(byte version, byte txPower) {
        return new AutoValue_BluetoothMetadata(version, txPower);
    }

    // Bits 7:6 = major version
    // Bits 5:4 = minor version
    // Bits 3:2 = transmit power calibration confidence (added in v1.1)
    // Bits 1:0 = reserved
    public abstract byte version();

    public abstract byte txPower();

    public byte[] toBytes() {
        // Byte 0 = version
        // Byte 1 = TX power
        // Byte 2/3 = reserved
        return new byte[]{version(), txPower(), 0, 0};
    }

    public CalibrationConfidence calibrationConfidence() {
        CalibrationConfidence confidence =
                CalibrationConfidence.forNumber(
                        (version() & CALIBRATION_CONFIDENCE_BITS_MASK) >> CALIBRATION_CONFIDENCE_BITS_OFFSET);
        if (confidence == null) {
            confidence = CalibrationConfidence.LOWEST_CONFIDENCE;
        }
        return confidence;
    }

    /**
     * Parse the {@link BluetoothMetadata} from a raw byte array.
     *
     * @param rawMetadata the decrypted raw metadata from {@link AssociatedEncryptedMetadata}.
     */
    public static BluetoothMetadata fromBytes(byte[] rawMetadata) {
        checkArgument(rawMetadata.length == 4);
        return BluetoothMetadata.create(/*version=*/ rawMetadata[0], /*txPower=*/ rawMetadata[1]);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data;

import org.joda.time.Instant;

import java.nio.ByteBuffer;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.concurrent.TimeUnit.DAYS;

/**
 * A day number is also known as exposure key rolling period number or EKRollingPeriod number. We
 * call it day number internally because the exposure key rolling period is 144 * 10 minutes or 24
 * hours. Each day number represents a 24-hour window that is based on Unix Epoch Time and is
 * timezone independent.
 *
 * <p>The value shall be in [0, 65535] interval. Though 18366 is the smallest legitimate day number
 * as of 2020/04/14, we allow the day number to be smaller than that to properly handle wrong system
 * clock on some devices.
 */
public class DayNumber {
    public static final DayNumber MIN_DAY_NUMBER = new DayNumber(0);
    public static final DayNumber MAX_DAY_NUMBER = new DayNumber(0xFFFF);
    private static final int SIZE_BYTES = 2;

    private final int value;

    public DayNumber(Instant instant) {
        value = getDayNumber(instant);
    }

    public DayNumber(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    /**
     * Puts the 2 bytes (big-endian) representation of this {@link DayNumber} in a {@link ByteBuffer}.
     * The 2 bytes can represent a day number till 2149/06/07.
     *
     * @param byteBuffer the destination buffer to {@link ByteBuffer#put} the byte representation.
     */
    public void putIn(ByteBuffer byteBuffer) {
        putIn(value, byteBuffer);
    }

    /**
     * The static version of {@link #putIn(ByteBuffer)}.
     */
    public static void putIn(int dayNumber, ByteBuffer byteBuffer) {
        checkArgument(byteBuffer.remaining() >= SIZE_BYTES);
        byteBuffer.putShort((short) dayNumber);
    }

    /**
     * Gets a {@link DayNumber} from the 2 bytes (big-endian) representation in a {@link ByteBuffer}.
     */
    public static DayNumber getFrom(ByteBuffer byteBuffer) {
        return new DayNumber(getValueFrom(byteBuffer));
    }

    /**
     * The static version of {@link #getFrom(ByteBuffer)}.
     */
    public static int getValueFrom(ByteBuffer byteBuffer) {
        checkArgument(byteBuffer.remaining() >= SIZE_BYTES);
        return ((int) byteBuffer.getShort()) & 0xFFFF; // Equivalent of Java 8 Short.toUnsignedInt().
    }

    /**
     * Returns the int representation of a day number of an {@link Instant}.
     *
     * @param instant the instant to get day number of
     * @return day number with the range in [0, 65535] interval, or undefined if the {@code instant}
     * is later than June 6, 2149 12:00:00 AM GMT.
     */
    public static int getDayNumber(Instant instant) {
        return (int) (instant.getMillis() / DAYS.toMillis(1));
    }

    public static int getSizeBytes() {
        return SIZE_BYTES;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data;

import com.google.auto.value.AutoValue;

/**
 * A {@link RollingProximityId} with a concept of what time interval number it was broadcasted for.
 *
 * <p>This class is intentionally different from {@link RollingProximityId} in that this class is
 * owned by system internally while {@link RollingProximityId}s are ids observed externally by
 * scanning.
 */
@AutoValue
public abstract class GeneratedRollingProximityId {

    /**
     * The {@link RollingProximityId} value.
     */
    public abstract RollingProximityId rollingProximityId();

    /**
     * The associated interval number that is encrypted inside of the {@link RollingProximityId}.
     */
    public abstract int intervalNumber();

    public static GeneratedRollingProximityId create(
            RollingProximityId rollingProximityId, int intervalNumber) {
        return new AutoValue_GeneratedRollingProximityId(rollingProximityId, intervalNumber);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data;

import com.google.samples.ByteArrayValue;

/**
 * Represents a Rolling Proximity Identifier.
 */
@SuppressWarnings("unused")
public final class RollingProximityId extends ByteArrayValue {

    private static final byte[] MIN_VALUE = {
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
    private static final byte[] MAX_VALUE = {
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF
    };
    public static final RollingProximityId MIN_ID = new RollingProximityId(MIN_VALUE);
    public static final RollingProximityId MAX_ID = new RollingProximityId(MAX_VALUE);

    public RollingProximityId(byte[] value) {
        super(value);
    }

    /**
     * Constructor which will take ownership of {@code value} if {@code takeOwnership} is true.
     *
     * <p>WARNING: This constructor can leave this {@link RollingProximityId} open to unexpected
     * mutation. Use only in cases where the {@code byte[]} is a temporary, to avoid the overhead of
     * cloning an array which will immediately go out of scope.
     */
    public RollingProximityId(byte[] value, boolean takeOwnership) {
        super(value, takeOwnership);
    }

    /**
     * Return a direct reference to the underlying data.
     *
     * <p>WARNING: This breaks the encapsulation and could allow the value to be mutated. Only use in
     * performance sensitive cases where the cost of cloning is too high, and the use of the {@code
     * byte[]} is guaranteed to be read-only.
     */
    public byte[] getDirect() {
        return exposeInternalBytesAndRiskMutation();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data;

import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * A utility class providing helper functions for {@link TemporaryExposureKey}.
 */
public final class TemporaryExposureKeySupport {
    private static final byte[] MIN_VALUE = {
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
    };
    private static final byte[] MAX_VALUE = {
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF,
            (byte) 0xFF
    };

    private TemporaryExposureKeySupport() {
    }

    public static DayNumber getDayNumber(TemporaryExposureKey temporaryExposureKey) {
        return new DayNumber(
                (int)
                        (temporaryExposureKey.getRollingStartIntervalNumber()
                                / ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod()));
    }

    /**
     * Gets the time which a key is released from embargo. This time is the key's expiration date (ie,
     * start period + number of rolling periods valid for) + an additional number of rolling periods
     * to protect against clock drift.
     */
    public static long getEmbargoEndTimeInMillis(TemporaryExposureKey temporaryExposureKey) {
        long endIntervalNumber =
                temporaryExposureKey.getRollingStartIntervalNumber()
                        + temporaryExposureKey.getRollingPeriod()
                        + ContactTracingFeature.tkMatchingClockDriftRollingPeriods();
        return endIntervalNumber * MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes());
    }

    public static int getRollingStartIntervalNumber(DayNumber dayNumber) {
        return getRollingStartIntervalNumber(dayNumber.getValue());
    }

    public static int getRollingStartIntervalNumber(int dayNumber) {
        return (int) (dayNumber * ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
    }

    /**
     * Get the rolling end interval number (exclusive). In another word, this number is the first
     * interval number that does not belong to this {@link TemporaryExposureKey} following the {@link
     * TemporaryExposureKey#getRollingStartIntervalNumber()}.
     */
    public static int getRollingEndIntervalNumber(TemporaryExposureKey temporaryExposureKey) {
        return temporaryExposureKey.getRollingStartIntervalNumber()
                + temporaryExposureKey.getRollingPeriod();
    }

    /**
     * Get the maximum possible rolling end interval number (exclusive). In another word, this number
     * is the first interval number that does not belong to this {@link TemporaryExposureKey}
     * following the {@link TemporaryExposureKey#getRollingStartIntervalNumber()} under assumption of
     * maximum key validity.
     */
    public static int getMaxPossibleRollingEndIntervalNumber(
            TemporaryExposureKey temporaryExposureKey) {
        return (int)
                (temporaryExposureKey.getRollingStartIntervalNumber()
                        + ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
    }

    /**
     * Get the end time of the validation window of {@code temporaryExposureKey} without considering
     * embargo. See {@link #getEmbargoEndTimeInMillis(TemporaryExposureKey)} for embargo end time.
     */
    public static long getExpirationTimeMillis(TemporaryExposureKey temporaryExposureKey) {
        return getRollingEndIntervalNumber(temporaryExposureKey)
                * MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes());
    }

    public static int getKeySizeBytes() {
        return (int) ContactTracingFeature.temporaryTracingKeySizeBytes();
    }

    public static TemporaryExposureKey getMinKey(DayNumber dayNumber) {
        return new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                .setRollingStartIntervalNumber(getRollingStartIntervalNumber(dayNumber))
                .setKeyData(MIN_VALUE)
                .build();
    }

    public static TemporaryExposureKey getMaxKey(DayNumber dayNumber) {
        return new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                .setRollingStartIntervalNumber(getRollingStartIntervalNumber(dayNumber))
                .setKeyData(MAX_VALUE)
                .build();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data.fileformat;

import com.google.protobuf.CodedInputStream;
import com.google.protobuf.ExtensionRegistryLite;
import com.google.protobuf.WireFormat;
import com.google.samples.exposurenotification.ExposureKeyExportProto;
import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKeyExport;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.matching.TemporaryExposureKeyExportV1Header;
import com.google.samples.exposurenotification.storage.CloseableIterable;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.NoSuchElementException;

/**
 * A class reads and parses a temporary exposure key export file in a streaming mode meaning that
 * the memory usage is independent to the file size.
 *
 * <p>See https://developers.google.com/android/exposure-notifications/exposure-key-file-format
 */
public class StreamingTemporaryExposureKeyFileParser {

    /**
     * Reads and parses the given {@link File} as {@link CloseableIterable<TemporaryExposureKey>}.
     *
     * <p>Parsing different files concurrently is safe. Parsing the same file before the previous
     * returned {@link CloseableIterable} closes is not allowed.
     *
     * @param file The key export file
     * @return A single-use iterable that must be closed after use. Not thread-safe.
     */
    public static CloseableIterable<TemporaryExposureKey> parse(File file) throws IOException {
        return ParserIterable.create(new FileInputStream(file));
    }

    /** Same as {@link #parse(File)} but takes an {@link InputStream} instead. */
    public static CloseableIterable<TemporaryExposureKey> parse(InputStream inputStream)
            throws IOException {
        return ParserIterable.create(inputStream);
    }

    /** The {@link CloseableIterable} that handles file reading while iteration. */
    private static class ParserIterable implements CloseableIterable<TemporaryExposureKey> {
        private final InputStream inputStream;
        private final CodedInputStream codedStream;

        // The tag (that has been read) for the field that yet to be read next. At anywhere outside of
        // readUntilNextKeyTagOrEnd(), the value of nextTag can only be KEYS_FIELD_NUMBER or 0 because
        // we always skip all other fields.
        private int nextTag;

        public static ParserIterable create(InputStream inputStream) throws IOException {
            ParserIterable parser = new ParserIterable(inputStream);

            // Prime the parser. Do this from outside of the constructor, to keep the nullness
            // checker happy.
            TemporaryExposureKeyExportV1Header.readAndVerifyHeader(inputStream);
            parser.readUntilNextKeyTagOrEnd();

            return parser;
        }

        private ParserIterable(InputStream inputStream) {
            this.inputStream = inputStream;
            codedStream = CodedInputStream.newInstance(inputStream);
        }

        @Override
        public void close() throws IOException {
            inputStream.close();
        }

        @Override
        public Iterator<TemporaryExposureKey> iterator() {
            return new Iterator<TemporaryExposureKey>() {
                @Override
                public boolean hasNext() {
                    return nextTag != 0;
                }

                @Override
                public TemporaryExposureKey next() {
                    return readNextKey();
                }
            };
        }

        private void readUntilNextKeyTagOrEnd() throws IOException {
            nextTag = codedStream.readTag();
            while (nextTag != 0 && !isTagForKeys(nextTag)) {
                codedStream.skipField(nextTag);
                nextTag = codedStream.readTag();
            }
        }

        private TemporaryExposureKey readNextKey() {
            if (!isTagForKeys(nextTag)) {
                throw new NoSuchElementException("Unexpected proto buffer field");
            }

            try {
                ExposureKeyExportProto.TemporaryExposureKey.Builder builder =
                        ExposureKeyExportProto.TemporaryExposureKey.newBuilder();
                codedStream.readMessage(builder, ExtensionRegistryLite.getEmptyRegistry());
                readUntilNextKeyTagOrEnd();
                return new TemporaryExposureKeyConverter().convert(builder.build());
            } catch (IOException e) {
                throw new NoSuchElementException("IOException: " + e.getMessage());
            }
        }
    }

    private static boolean isTagForKeys(int tag) {
        return WireFormat.getTagFieldNumber(tag) == TemporaryExposureKeyExport.KEYS_FIELD_NUMBER;
    }

    private StreamingTemporaryExposureKeyFileParser() {}
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data.fileformat;


import com.google.common.base.Converter;
import com.google.samples.exposurenotification.ExposureKeyExportProto;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;

/**
 * Converts temporary exposure key from the export proto format to Android parsable format.
 */
public class TemporaryExposureKeyConverter
        extends Converter<ExposureKeyExportProto.TemporaryExposureKey, TemporaryExposureKey> {

    @Override
    protected TemporaryExposureKey doForward(ExposureKeyExportProto.TemporaryExposureKey exportKey) {
        return new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                .setKeyData(exportKey.getKeyData().toByteArray())
                .setRollingStartIntervalNumber(exportKey.getRollingStartIntervalNumber())
                .setRollingPeriod(exportKey.getRollingPeriod())
                .setTransmissionRiskLevel(exportKey.getTransmissionRiskLevel())
                .build();
    }

    @Override
    protected ExposureKeyExportProto.TemporaryExposureKey doBackward(
            TemporaryExposureKey temporaryExposureKey) {
        throw new UnsupportedOperationException(
                "Converting from Android parceable to export proto is not supported");
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data.generator;

import android.os.Build.VERSION_CODES;

import androidx.annotation.RequiresApi;

import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.crypto.CryptoException;
import com.google.samples.exposurenotification.crypto.KeyDerivation;
import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
import com.google.samples.exposurenotification.data.BluetoothMetadata;
import com.google.samples.exposurenotification.data.RollingProximityId;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Mac;

/**
 * The class to encrypt (from {@link BluetoothMetadata} to {@link AssociatedEncryptedMetadata}) and
 * decrypt (opposite) metadata.
 */
@RequiresApi(api = VERSION_CODES.KITKAT)
public class AssociatedEncryptedMetadataGenerator {
    private final byte[] aemKey;

    /**
     * Construct a {@link AssociatedEncryptedMetadataGenerator} with a {@link TemporaryExposureKey}
     * which is required to encrypt and decrypt metadata.
     *
     * @deprecated Use {@link Factory} to create {@link AssociatedEncryptedMetadataGenerator}
     * instances.
     */
    @Deprecated
    public AssociatedEncryptedMetadataGenerator(TemporaryExposureKey temporaryExposureKey)
            throws CryptoException {
        aemKey =
                AssociatedEncryptedMetadataHelper.generateAemKey(
                        temporaryExposureKey.getKeyData(),
                        ContactTracingFeature.aemkHkdfInfoString(),
                        (int) ContactTracingFeature.associatedMetadataEncryptionKeySizeBytes());
    }

    /**
     * Construct a {@link AssociatedEncryptedMetadataGenerator} with a {@link TemporaryExposureKey}
     * which is required to encrypt and decrypt metadata.
     */
    private AssociatedEncryptedMetadataGenerator(
            Mac mac, TemporaryExposureKey temporaryExposureKey, byte[] aemkHkdfInfoBytes)
            throws CryptoException {
        aemKey =
                AssociatedEncryptedMetadataHelper.generateAemKey(
                        mac,
                        temporaryExposureKey.getKeyData(),
                        aemkHkdfInfoBytes,
                        (int) ContactTracingFeature.associatedMetadataEncryptionKeySizeBytes());
    }

    /**
     * Encrypts BLE metadata using AES-CTR(AEMK, RPI, metadata).
     */
    public AssociatedEncryptedMetadata encrypt(
            RollingProximityId rollingProximityId, BluetoothMetadata metadata) throws CryptoException {
        return AssociatedEncryptedMetadataHelper.encrypt(rollingProximityId.get(), metadata, aemKey);
    }

    /**
     * Decrypts using AES-CTR(AEMK, RPI, encryptedData).
     */
    public BluetoothMetadata decrypt(
            RollingProximityId rollingProximityId, AssociatedEncryptedMetadata aem)
            throws CryptoException {
        return BluetoothMetadata.fromBytes(
                AssociatedEncryptedMetadataHelper.encryptOrDecrypt(
                        aemKey, rollingProximityId.getDirect(), aem.getDirect()));
    }

    /**
     * Factory to create {@link AssociatedEncryptedMetadataGenerator}.
     */
    public static class Factory {
        private final Mac mac;

        public Factory() throws CryptoException {
            try {
                mac = Mac.getInstance(KeyDerivation.ALGORITHM_NAME);
            } catch (NoSuchAlgorithmException e) {
                throw new CryptoException(e);
            }
        }

        /**
         * Gets an instance of {@link AssociatedEncryptedMetadataGenerator}.
         */
        public AssociatedEncryptedMetadataGenerator getInstance(
                TemporaryExposureKey temporaryExposureKey) throws CryptoException {
            return new AssociatedEncryptedMetadataGenerator(
                    mac,
                    temporaryExposureKey,
                    ContactTracingFeature.aemkHkdfInfoString().getBytes(StandardCharsets.UTF_8));
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data.generator;

import com.google.samples.exposurenotification.crypto.AesCtrEncryptor;
import com.google.samples.exposurenotification.crypto.CryptoException;
import com.google.samples.exposurenotification.crypto.KeyDerivation;
import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
import com.google.samples.exposurenotification.data.BluetoothMetadata;

import javax.crypto.Mac;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * Helper class for the {@link AssociatedEncryptedMetadataGenerator} .
 */
public class AssociatedEncryptedMetadataHelper {

    /**
     * Generates the Associated Encryption Metadata key by the given temporary exposure key.
     *
     * @deprecated use {@link #generateAemKey(Mac, byte[], byte[], int)} instead.
     */
    @Deprecated
    @SuppressWarnings("NewApi")
    public static byte[] generateAemKey(
            byte[] temporaryExposureKey,
            String aemkHkdfInfoString,
            int associatedMetadataEncryptionKeySizeBytes)
            throws CryptoException {
        // AEMK = HKDF(tek, NULL, UTF8("EN-AEMK"), 16).
        return KeyDerivation.hkdfSha256(
                temporaryExposureKey,
                /* inputSalt =*/ null,
                aemkHkdfInfoString.getBytes(UTF_8),
                associatedMetadataEncryptionKeySizeBytes);
    }

    /**
     * Generates the Associated Encryption Metadata key by the given temporary exposure key.
     */
    @SuppressWarnings("NewApi")
    public static byte[] generateAemKey(
            Mac mac,
            byte[] temporaryExposureKey,
            byte[] aemkHkdfInfoBytes,
            int associatedMetadataEncryptionKeySizeBytes)
            throws CryptoException {
        // AEMK = HKDF(tek, NULL, UTF8("EN-AEMK"), 16).
        return KeyDerivation.hkdfSha256(
                mac,
                temporaryExposureKey,
                /* inputSalt =*/ null,
                aemkHkdfInfoBytes,
                associatedMetadataEncryptionKeySizeBytes);
    }

    /**
     * Encrypt BLE metadata using AES-CTR(AEMK, RPI, metadata).
     */
    public static byte[] encryptOrDecrypt(
            byte[] aemKey, byte[] rollingProximityId, byte[] bluetoothMetadataAsBytes)
            throws CryptoException {
        return AesCtrEncryptor.aesCtr(aemKey, rollingProximityId, bluetoothMetadataAsBytes);
    }

    /**
     * Create an {@link AssociatedEncryptedMetadata} encrypted via {@code aemKey}.
     */
    public static AssociatedEncryptedMetadata encrypt(
            byte[] rollingProximityIdAsBytes, BluetoothMetadata metadata, byte[] aemKey)
            throws CryptoException {
        return AssociatedEncryptedMetadata.create(
                AssociatedEncryptedMetadataHelper.encryptOrDecrypt(
                        aemKey, rollingProximityIdAsBytes, metadata.toBytes()));
    }

    private AssociatedEncryptedMetadataHelper() {
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data.generator;

import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.crypto.AesEcbEncryptor;
import com.google.samples.exposurenotification.crypto.CryptoException;
import com.google.samples.exposurenotification.crypto.KeyDerivation;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import java.security.NoSuchAlgorithmException;
import java.time.Instant;

import javax.crypto.Mac;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * A class to generate {@code RollingProximityId}(s).
 */
public class RollingProximityIdGenerator extends RollingProximityIdGeneratorBase {

    /**
     * Used primarily to avoid re-creating {@link AesEcbEncryptor} for performance.
     *
     * @deprecated Use {@link Factory} to create {@link RollingProximityIdGenerator} instances.
     */
    @Deprecated
    public RollingProximityIdGenerator(
            AesEcbEncryptor aesEcbEncryptor,
            byte[] keyData,
            int rollingStartIntervalNumber,
            int rollingEndIntervalNumber,
            int rollingProximityIdKeySizeBytes,
            String rpikHkdfInfoString,
            String rpidAesPaddedString)
            throws CryptoException {
        super(
                aesEcbEncryptor,
                keyData,
                rollingStartIntervalNumber,
                rollingEndIntervalNumber,
                rollingProximityIdKeySizeBytes,
                rpikHkdfInfoString,
                rpidAesPaddedString);
    }

    /**
     * Used primarily to avoid re-creating {@link Mac}, and {@link AesEcbEncryptor} for performance.
     */
    private RollingProximityIdGenerator(
            Mac mac,
            AesEcbEncryptor aesEcbEncryptor,
            byte[] keyData,
            int rollingStartIntervalNumber,
            int rollingEndIntervalNumber,
            int rollingProximityIdKeySizeBytes,
            byte[] rpikHkdfInfoBytes,
            byte[] rpidAesPaddedBytes,
            PaddedDataCache paddedDataCache)
            throws CryptoException {
        super(
                mac,
                aesEcbEncryptor,
                keyData,
                rollingStartIntervalNumber,
                rollingEndIntervalNumber,
                rollingProximityIdKeySizeBytes,
                rpikHkdfInfoBytes,
                rpidAesPaddedBytes,
                paddedDataCache);
    }

    /**
     * @deprecated Use {@link Factory} to create {@link RollingProximityIdGenerator} instances.
     */
    @Deprecated
    public RollingProximityIdGenerator(
            byte[] keyData,
            int rollingStartIntervalNumber,
            int rollingEndIntervalNumber,
            int rollingProximityIdKeySizeBytes,
            String rpikHkdfInfoString,
            String rpidAesPaddedString)
            throws CryptoException {
        this(
                AesEcbEncryptor.create(),
                keyData,
                rollingStartIntervalNumber,
                rollingEndIntervalNumber,
                rollingProximityIdKeySizeBytes,
                rpikHkdfInfoString,
                rpidAesPaddedString);
    }

    /**
     * Factory to create {@link RollingProximityIdGenerator}.
     */
    public static class Factory {
        private final Mac mac;
        private final PaddedDataCache paddedDataCache;

        public Factory() throws CryptoException {
            int startRollingEnIntervalNumber = getStartRollingEnIntervalNumber(Instant.now());
            int baseEnIntervalNumber =
                    startRollingEnIntervalNumber
                            - (int)
                            (ContactTracingFeature.contactRecordDaysToKeep()
                                    * ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
            paddedDataCache =
                    PaddedDataCache.createInstance(
                            baseEnIntervalNumber,
                            (int) (ContactTracingFeature.contactRecordDaysToKeep() + 1),
                            ContactTracingFeature.rpidAesPaddedString().getBytes(UTF_8),
                            (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());

            try {
                mac = Mac.getInstance(KeyDerivation.ALGORITHM_NAME);
            } catch (NoSuchAlgorithmException e) {
                throw new CryptoException(e);
            }
        }

        /**
         * Gets an instance of {@link RollingProximityIdGenerator}.
         */
        public RollingProximityIdGenerator getInstance(
                AesEcbEncryptor aesEcbEncryptor,
                byte[] keyData,
                int rollingStartIntervalNumber,
                int rollingEndIntervalNumber,
                int rollingProximityIdKeySizeBytes,
                String rpikHkdfInfoString,
                String rpidAesPaddedString)
                throws CryptoException {
            if (rollingEndIntervalNumber - rollingStartIntervalNumber > 144) {
                Log.log
                        .atWarning()
                        .log("RPI generator window is greater than 144. startIntervalNumber=%d,"
                                        + " endIntervalNumber=%d",
                                rollingStartIntervalNumber, rollingEndIntervalNumber);
            }
            return new RollingProximityIdGenerator(
                    mac,
                    aesEcbEncryptor,
                    keyData,
                    rollingStartIntervalNumber,
                    rollingEndIntervalNumber,
                    rollingProximityIdKeySizeBytes,
                    rpikHkdfInfoString.getBytes(UTF_8),
                    rpidAesPaddedString.getBytes(UTF_8),
                    paddedDataCache);
        }

        private static int getStartRollingEnIntervalNumber(Instant instant) {
            int tkRollingPeriod =
                    (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod();
            return tkRollingPeriod * ((int) (instant.toEpochMilli()
                    / MINUTES.toMillis(ContactTracingFeature.idRollingPeriodMinutes()))
                    / tkRollingPeriod);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data.generator;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.google.common.collect.ImmutableList;
import com.google.samples.exposurenotification.crypto.AesEcbEncryptor;
import com.google.samples.exposurenotification.crypto.CryptoException;
import com.google.samples.exposurenotification.crypto.KeyDerivation;
import com.google.samples.exposurenotification.data.GeneratedRollingProximityId;
import com.google.samples.exposurenotification.data.RollingProximityId;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

import javax.crypto.Mac;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * A class to generate {@link RollingProximityId}(s).
 */
public class RollingProximityIdGeneratorBase {
    private static final int AES_BLOCK_SIZE = 16;
    private final int rollingStartIntervalNumber;
    private final int rollingEndIntervalNumber;
    private final AesEcbEncryptor encryptor;
    private final byte[] aesPadding;
    @Nullable
    private PaddedDataCache paddedDataCache;

    /**
     * Used primarily to avoid re-creating {@link AesEcbEncryptor} for performance.
     *
     * @deprecated use {@link #RollingProximityIdGeneratorBase(Mac, AesEcbEncryptor, byte[], int, int,
     * int, byte[], byte[], PaddedDataCache)} instead.
     */
    @Deprecated
    @SuppressWarnings("NewApi")
    public RollingProximityIdGeneratorBase(
            AesEcbEncryptor aesEcbEncryptor,
            byte[] keyData,
            int rollingStartIntervalNumber,
            int rollingEndIntervalNumber,
            int rollingProximityIdKeySizeBytes,
            String rpikHkdfInfoString,
            String rpidAesPaddedString)
            throws CryptoException {
        this.rollingStartIntervalNumber = rollingStartIntervalNumber;
        this.rollingEndIntervalNumber = rollingEndIntervalNumber;
        aesPadding = rpidAesPaddedString.getBytes(UTF_8);
        encryptor = aesEcbEncryptor;
        encryptor.init(generateRpiKey(keyData, rpikHkdfInfoString, rollingProximityIdKeySizeBytes));
    }

    /**
     * Used primarily to avoid re-creating {@link Mac}, and {@link AesEcbEncryptor} for performance.
     */
    @SuppressWarnings("NewApi")
    public RollingProximityIdGeneratorBase(
            Mac mac,
            AesEcbEncryptor aesEcbEncryptor,
            byte[] keyData,
            int rollingStartIntervalNumber,
            int rollingEndIntervalNumber,
            int rollingProximityIdKeySizeBytes,
            byte[] rpikHkdfInfoBytes,
            byte[] rpidAesPaddedBytes,
            PaddedDataCache paddedDataCache)
            throws CryptoException {
        this.rollingStartIntervalNumber = rollingStartIntervalNumber;
        this.rollingEndIntervalNumber = rollingEndIntervalNumber;
        this.paddedDataCache = paddedDataCache;
        aesPadding = rpidAesPaddedBytes;
        encryptor = aesEcbEncryptor;
        encryptor.init(generateRpiKey(mac, keyData, rpikHkdfInfoBytes, rollingProximityIdKeySizeBytes));
    }

    public RollingProximityIdGeneratorBase(
            byte[] keyData,
            int rollingStartIntervalNumber,
            int rollingEndIntervalNumber,
            int rollingProximityIdKeySizeBytes,
            String rpikHkdfInfoString,
            String rpidAesPaddedString)
            throws CryptoException {
        this(
                AesEcbEncryptor.create(),
                keyData,
                rollingStartIntervalNumber,
                rollingEndIntervalNumber,
                rollingProximityIdKeySizeBytes,
                rpikHkdfInfoString,
                rpidAesPaddedString);
    }

    /**
     * Generates a list of rolling proximity ids.
     */
    @SuppressWarnings("NewApi")
    public ImmutableList<GeneratedRollingProximityId> generateIds(byte[] reusedOutput)
            throws CryptoException {
        int numIds = rollingEndIntervalNumber - rollingStartIntervalNumber;
        byte[] paddedData = null;
        if (paddedDataCache != null && numIds <= paddedDataCache.getIdsPerKey()) {
            paddedData = paddedDataCache.getCachedData(rollingStartIntervalNumber);
        }
        if (paddedData == null) {
            paddedData = generateAggregatePaddedData(rollingStartIntervalNumber, numIds, aesPadding);
        }
        return convertToList(encryptor.encrypt(paddedData, reusedOutput), numIds);
    }

    /**
     * Generates the Rolling Proximity Id key by the given temporary exposure key.
     */
    @VisibleForTesting
    @SuppressWarnings("NewApi")
    static byte[] generateRpiKey(
            byte[] temporaryExposureKey, String rpikHkdfInfoString, int rollingProximityIdKeySizeBytes)
            throws CryptoException {
        // RPIK = HKDF(tek, NULL, UTF8("EN-RPIK"), 16).
        return KeyDerivation.hkdfSha256(
                temporaryExposureKey,
                /* inputSalt =*/ null,
                rpikHkdfInfoString.getBytes(UTF_8),
                rollingProximityIdKeySizeBytes);
    }

    /**
     * Generates the Rolling Proximity Id key by the given temporary exposure key.
     */
    @SuppressWarnings("NewApi")
    private static byte[] generateRpiKey(
            Mac mac,
            byte[] temporaryExposureKey,
            byte[] rpikHkdfInfoBytes,
            int rollingProximityIdKeySizeBytes)
            throws CryptoException {
        // RPIK = HKDF(tek, NULL, UTF8("EN-RPIK"), 16).
        return KeyDerivation.hkdfSha256(
                mac,
                temporaryExposureKey,
                /* inputSalt =*/ null,
                rpikHkdfInfoBytes,
                rollingProximityIdKeySizeBytes);
    }

    /**
     * Generates a single rolling proximity id using AES(RPIK, PaddedData).
     *
     * @param intervalNumber index of 10-minute interval since Epoch.
     */
    public RollingProximityId generateId(int intervalNumber) throws CryptoException {
        return new RollingProximityId(
                encryptor.encrypt(generatePaddedData(intervalNumber, aesPadding)), /*takeOwnership=*/ true);
    }

    private ImmutableList<GeneratedRollingProximityId> convertToList(
            byte[] rawRollingProximityIds, int numIds) {
        ImmutableList.Builder<GeneratedRollingProximityId> generatedIds = ImmutableList.builder();
        for (int i = 0; i < numIds; ++i) {
            generatedIds.add(
                    GeneratedRollingProximityId.create(
                            new RollingProximityId(
                                    Arrays.copyOfRange(
                                            rawRollingProximityIds, i * AES_BLOCK_SIZE, (i + 1) * AES_BLOCK_SIZE),
                                    true),
                            rollingStartIntervalNumber + i));
        }
        return generatedIds.build();
    }

    private static byte[] generatePaddedData(int intervalNumber, byte[] aesPadding) {
        ByteBuffer paddedData = ByteBuffer.allocate(AES_BLOCK_SIZE).order(ByteOrder.LITTLE_ENDIAN);
        return addPaddedData(paddedData, intervalNumber, aesPadding).array();
    }

    protected static byte[] generateAggregatePaddedData(
            int startIntervalNumber, int numIds, byte[] aesPadding) {
        ByteBuffer allPaddedData =
                ByteBuffer.allocate(AES_BLOCK_SIZE * numIds).order(ByteOrder.LITTLE_ENDIAN);
        for (int i = 0; i < numIds; ++i) {
            allPaddedData = addPaddedData(allPaddedData, startIntervalNumber + i, aesPadding);
        }
        return allPaddedData.array();
    }

    /**
     * Padded data is the following sequence of 16 bytes:
     *
     * <ol>
     *   <li>PaddedData[0 - 5]: "EN-RPI".getBytes(UTF_8).
     *   <li>PaddedData[6 - 11]: 0x000000000000.
     *   <li>PaddedData[12 - 15]: intervalNumber, uint32 little-endian.
     * </ol>
     *
     * <p>{@code buffer} must be zero filled.
     */
    private static ByteBuffer addPaddedData(
            ByteBuffer buffer, int intervalNumber, byte[] aesPadding) {
        buffer
                .put(aesPadding)
                .position(buffer.position() + AES_BLOCK_SIZE - aesPadding.length - 4); // 4 is Integer.BYTES
        buffer.putInt(intervalNumber);
        return buffer;
    }

    static class PaddedDataCache {
        private final int baseEnIntervalNumber;
        private final int size;
        private final int idsPerKey;
        private final byte[][] cachedData;

        public static PaddedDataCache createInstance(
                int baseEnIntervalNumber, int cacheSize, byte[] aesPadding, int idsPerKey) {
            return new PaddedDataCache(
                    createPaddedDataCache(baseEnIntervalNumber, cacheSize, aesPadding, idsPerKey),
                    baseEnIntervalNumber,
                    cacheSize,
                    idsPerKey);
        }

        private PaddedDataCache(
                byte[][] cachedData, int baseEnIntervalNumber, int size, int idsPerKey) {
            this.baseEnIntervalNumber = baseEnIntervalNumber;
            this.cachedData = cachedData;
            this.size = size;
            this.idsPerKey = idsPerKey;
        }

        int getSize() {
            return size;
        }

        int getIdsPerKey() {
            return idsPerKey;
        }

        @Nullable
        public byte[] getCachedData(int enIntervalNumber) {
            int index = getIndex(enIntervalNumber, baseEnIntervalNumber);
            return (index >= 0 && index < getSize()) ? cachedData[index] : null;
        }

        private int getIndex(int enIntervalNumber, int baseEnIntervalNumber) {
            // The given enIntervalNumber should be aligned to idsPerKey.
            return (((enIntervalNumber - baseEnIntervalNumber) % idsPerKey) == 0)
                    ? ((enIntervalNumber - baseEnIntervalNumber) / idsPerKey)
                    : -1;
        }

        private static byte[][] createPaddedDataCache(
                int baseEnIntervalNumber, int size, byte[] aesPadding, int idsPerKey) {
            byte[][] paddedDataSet = new byte[size][];
            for (int i = 0, intervalNumber = baseEnIntervalNumber;
                 i < size;
                 i++, intervalNumber += idsPerKey) {
                paddedDataSet[i] = generateAggregatePaddedData(intervalNumber, idsPerKey, aesPadding);
            }
            return paddedDataSet;
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.data.generator;

import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import java.security.SecureRandom;

/**
 * Generator for {@link TemporaryExposureKey}.
 */
public final class TemporaryExposureKeyGenerator {
    private TemporaryExposureKeyGenerator() {
    }

    public static TemporaryExposureKey generateKey(int rollingStartIntervalNumber) {
        return generateKey(
                rollingStartIntervalNumber,
                (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod());
    }

    public static TemporaryExposureKey generateKey(
            int rollingStartIntervalNumber, int rollingPeriod) {
        SecureRandom random = new SecureRandom();
        byte[] key = new byte[ContactTracingFeature.temporaryTracingKeySizeBytes()];
        random.nextBytes(key);
        return new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                .setKeyData(key)
                .setRollingStartIntervalNumber(rollingStartIntervalNumber)
                .setRollingPeriod(rollingPeriod)
                .build();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.features;

import android.bluetooth.le.ScanSettings;

import com.google.common.primitives.Ints;

import java.util.Arrays;
import java.util.List;

/**
 * This class contains various constants and feature flags used through the exposure notifications
 * system. Many of these values should ideally be delivered in a way that they can be updated
 * without needing to push a new version of code. For example, the method
 * {@link #partnerPublicKeys()} returns the public keys of all currently active partners using
 * the system.
 * FIXME: Implement this as a deserialized object or some sort of key/value store.
 */
public class ContactTracingFeature {
    /**
     * The input parameter as HKDF info to generate AEMK.
     */
    public static String aemkHkdfInfoString() {
        return "EN-AEMK";
    }

    /**
     * The length of associated metadata encryption key in bytes.
     */
    public static int associatedMetadataEncryptionKeySizeBytes() {
        return 16;
    }

    /**
     * The length for which a Temporary Tracing Key is valid in multiple of id rolling period.
     */
    public static int tkRollingPeriodMultipleOfIdRollingPeriod() {
        return 144;
    }

    /**
     * The length of temporary tracing key in bytes.
     */
    public static int temporaryTracingKeySizeBytes() {
        return 16;
    }

    /**
     * The number of rolling periods that can be used before and after an RPIs validity time
     * during matching.
     */
    public static int tkMatchingClockDriftRollingPeriods() {
        return 12;
    }

    /**
     * The length for which an advertising ID is valid.
     */
    public static int idRollingPeriodMinutes() {
        return 10;
    }

    /**
     * The length of rolling proximity ID key in bytes.
     */
    public static int rollingProximityIdKeySizeBytes() {
        return 16;
    }

    /**
     * The input parameter as HKDF info to generate RPIK.
     */
    public static String rpikHkdfInfoString() {
        return "EN-RPIK";
    }

    /**
     * The padded constant for generating rolling proximity IDs
     */
    public static String rpidAesPaddedString() {
        return "EN-RPI";
    }

    /**
     * The number of (full) days to keep the contact record data
     */
    public static int contactRecordDaysToKeep() {
        return 14;
    }

    /**
     * The transmit power as would be reported by an average iPhone
     * transmitting at the same power as the Android device running with
     * ADVERTISE_TX_POWER_LOW
     */
    public static int txCalibrationPower() {
        return 0;
    }

    /**
     * Offset to add to the tx calibration power to obtain the tx
     * power to publish in the payload
     */
    public static int txCalibrationPowerToTxPower() {
        return 0;
    }

    /**
     * If enabled, the v1.1 metadata format will be advertised which
     * includes the calibration confidence value.
     */
    public static boolean advertisementMetadataV11() {
        return true;
    }

    /**
     * The confidence value for tx_calibration_power and rssi_offset.
     * Higher value means higher confidence.
     * Sees: exposurenotification/src/main/proto/calibration_enums.proto
     */
    public static byte calibrationConfidence() {
        return 0;
    }

    /**
     * The length of contact ID.
     */
    public static int contactIdLength() {
        return 16;
    }

    /**
     * Value of ADVERTISE_MODE_X enum that should be used.
     */
    public static int advertiseMode() {
        return 1; // BALANCED (250ms)
    }

    /**
     * Value of ADVERTISE_TX_POWER_X enum that should be used
     */
    public static int advertiseTxPower() {
        return 1;  // LOW (-15dB)
    }

    /**
     * Whether the advertisement should be connectable
     */
    public static boolean advertiseConnectable() {
        return false;
    }

    /**
     * Whether the BLE advertiser is legacy mode, will only take effect for O and above
     */
    public static boolean advertiseLegacyMode() {
        return true;
    }

    /**
     * The BLE advertising interval, will only take effect for O and above
     */
    public static int advertiseInterval() {
        return 400;  // AdvertisingSetParameters.INTERVAL_MEDIUM
    }

    /**
     * Value of ADVERTISE_TX_POWER_X enum that should be used
     */
    public static int advertiseTxPowerO() {
        return -15; // AdvertisingSetParameters.TX_POWER_LOW
    }

    /**
     * Whether the BLE advertiser is scannable, will only take effect for O and above
     */
    public static boolean advertiseScannable() {
        return false;
    }

    /**
     * Whether using startAdvertisingSet to start advertising for O and above
     */
    public static boolean advertiseUseOreoAdvertiser() {
        return false;
    }

    /**
     * File name of signature inside diagnosis keys zip
     */
    public static String diagnosisKeySignatureFileName() {
        return "export.sig";
    }

    /**
     * File name of key data binary inside diagnosis keys zip
     */
    public static String diagnosisKeyBinFileName() {
        return "export.bin";
    }

    /**
     * Calculates the hash of all keys when provideDiagnosisKeys is called.
     */
    public static boolean calculateDiagnosisKeyHash() {
        return true;
    }

    /**
     * Maximum permissible batch size group size of signature
     */
    public static int signatureMaxAllowedBatchGroupSize() {
        return 1_000_000;
    }

    /**
     * If enabled, signature verification would accept multiple whole batches
     */
    public static boolean enableMultipleWholeBatchSignatureVerification() {
        return true;
    }

    /**
     * Allow signature files to have signatures without a matching public key, or more matching
     * public keys than signatures, during verification.
     */
    public static boolean ignoreUnmatchedSignatures() {
        return true;
    }

    /**
     * A list of partner public keys for diagnosis key signature verification.
     */
    public static List<String> partnerPublicKeys() {
        return Arrays.asList(TEST_PARTNER_PUBLIC_KEYS);
    }

    /**
     * each list entry is a string formatted as:
     * <my.package.name>:<COSIGN_SET>|<COSIGN_SET>|...
     * <p>
     * COSIGN_SET:
     * <PUBLIC_KEY>&<PUBLIC_KEY>&...
     * <p>
     * PUBLIC_KEY:
     * <NAME>,<BASE64KEYVALUE>
     * <p>
     * NAME:
     * <KEY_ID>-<KEY_VERSION>
     * <p>
     * KEY_ID and KEY_VERSION:
     * arbitrary alphanumeric identifiers. Match verification_key_id
     * and verification_key_version in SignatureInfo fields of the
     * diagnosis export proto. Should definitely not contain any separator
     * character used in the flag definition. A name must be defined here, but
     * verification_key_id may be omitted in the proto if there is only a
     * single signature in the proto.
     * <p>
     * each co-signing set may include multiple key values, where all values
     * within a set are verified together. multiple co-signing sets may be
     * specified in order to support key rotations.
     * <p>
     * FIXME: Deliver authorized PHA public keys to devices.
     */
    private static final String[] TEST_PARTNER_PUBLIC_KEYS = {
            "com.google.android.apps.exposurenotification:ExampleServer-v1,MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUczyMAkfSeoU77Nmcb1G7t7xyGCAhQqMOIVDFLFas3J+elP7CiotovigCLWj706F07j1EPL27ThRzZl7Ha9uOA==|310-v1,MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE49JY6kekDgxj3Crm4y6kEHdfoKQFSNDM4mV9cgDb+e5nOAw0GeRoRThCu9/wX5wDT2QloFoOjl2pGZHI0f3C3w=="
    };

    /**
     * If enabled, will read full header
     */
    public static boolean readFullMetadataHeader() {
        return true;
    }

    /**
     * The Settings.Global name for the BLE low latency scan window value.
     */
    public static String lowLatencyScanWindowSettingsName() {
        return "ble_scan_low_latency_window_ms";
    }

    /**
     * The Settings.Global name for the BLE low latency scan interval value.
     */
    public static String lowLatencyScanIntervalSettingsName() {
        return "ble_scan_low_latency_interval_ms";
    }

    /**
     * The default low latency scan window value to be set when scanning for EN is inactive.
     */
    public static int lowLatencyScanWindowDefaultValueMillis() {
        return 4096;
    }

    /**
     * The default low latency scan interval value to be set when scanning for EN is inactive.
     */
    public static int lowLatencyScanIntervalDefaultValueMillis() {
        return 4096;
    }

    /**
     * Minimum Scan interval between each scan.
     */
    public static int minScanIntervalSeconds() {
        return 60;
    }

    /**
     * Scan interval between each scan.
     */
    public static int scanIntervalSeconds() {
        return 300; // 5 * 60
    }

    /**
     * Ratio for scan interval while audio playback. [0.1-1]
     */
    public static float scanIntervalRatioWhenAudioPlayback() {
        return 0.6f;
    }

    /**
     * Ratio for scan interval while screen on. [0.1-1]
     */
    public static float scanIntervalRatioWhenScreenOn() {
        return 0.8f;
    }

    /**
     * If enabled, the BLE scanning interval and window will be shrunk so that over the course of
     * the BLE scan, we use all 3 channels instead of only 1.
     */
    public static boolean bleScanningAdjustIntervalAndWindowValues() {
        return true;
    }

    /**
     * Offset to add to the measured RSSI to obtain a calibrated RSS.
     * <p>
     * The offset in measured rssi between an average iphone and a test
     * device. Experimentally determined on the most popular devices, and
     * extrapolated for similar models.
     * calibrated_rssi = measured_rssi + rssi_offset
     */
    public static int rssiOffset() {
        return 0;
    }

    /**
     * Random parameter for scan interval. The actual scan interval will be
     * scan_interval_seconds - RandomInt() % scan_interval_random_range_seconds,
     * and by default it could be 210 ~ 300 seconds.
     */
    public static int scanIntervalRandomRangeSeconds() {
        return 90; // 1.5 * 60
    }

    /**
     * Scan time for piggyback scan with other app's wake ups.
     */
    public static int piggybackScanTimeSeconds() {
        return 2;
    }

    /**
     * Scan time for each scan.
     */
    public static int scanTimeSeconds() {
        return 4;
    }

    /**
     * Additional scan time while some specific bluetooth profile is in use. If condition
     * matches, the total scan time will be scan_time_seconds +
     * scan_time_extend_for_profile_in_use_seconds,
     * by default it will be 15 seconds.
     */
    public static int scanTimeExtendForProfileInUseSeconds() {
        return 13;
    }

    /**
     * Only extend scan while bluetooth audio streaming or while in calls.
     */
    public static boolean extendScanOnlyForBtAudioOrCalls() {
        return true;
    }

    /**
     * Interval for piggyback scan with other app's wake ups.
     */
    public static int piggybackScanIntervalSeconds() {
        return 60;
    }

    /**
     * Delay for scheduled piggyback scan task while device idle, in seconds.
     */
    public static int piggybackScanDelayWhileDeviceIdleSeconds() {
        return 5;
    }

    /**
     * Delay for scheduled piggyback scan task while screen off, in seconds.
     */
    public static int piggybackScanDelayWhileScreenOffSeconds() {
        return 15;
    }

    /**
     * Threshold to determine that if the piggyback task is executed while cpu sleeping. If the
     * task is delayed for more than this value, then we assume that cpu is during idle mode
     * recently.
     */
    public static int piggybackAccetableMinDelaySeconds() {
        return 1;
    }

    /**
     * Master switch for piggyback scan.
     */
    public static boolean enablePiggybackScan() {
        return false;
    }

    /**
     * Whether the BLE scanner accepts only legacy advertising or not, will only take effect for
     * O and above
     */
    public static boolean scanOnlyLegacy() {
        return false;
    }

    /**
     * Hold wakelock for scanning to prevent cpu going to sleep while scanning and delay the stop
     * scan schedule.
     */
    public static boolean wakelockForScan() {
        return true;
    }

    /**
     * Use opportunistic scan between regular scans.
     */
    public static boolean enableOpportunisticScan() {
        return false;
    }

    /**
     * Scan mode for contact scan.
     */
    public static int scanMode() {
        return ScanSettings.SCAN_MODE_LOW_LATENCY;
    }

    /**
     * By design, every opportunistic scan will reschedule the low latency scan to 5 minutes
     * later, but it will reschedule too frequently if too many opportunistic scan happen in a in
     * short period. This constant is used for the minimal reschedule interval.
     */
    public static int rescheduleStartScanMinIntervalMs() {
        return 10000; // 10 * 1000
    }

    /**
     * Bucket bounds for risk score latency days.
     */
    public static List<Integer> riskScoreLatencyDaysBuckets() {
        int[] riskScoreLatencyDaysBuckets = new int[]{
                14,
                12,
                10,
                8,
                6,
                4,
                2,
        };
        return Ints.asList(riskScoreLatencyDaysBuckets);
    }

    /**
     * Bucket bounds for risk score duration.
     */
    public static List<Integer> riskScoreDurationBuckets() {
        int[] riskScoreDurationBuckets = new int[]{
                0,
                5,
                10,
                15,
                20,
                25,
                30,
        };
        return Ints.asList(riskScoreDurationBuckets);
    }

    public static List<Integer> riskScoreAttenuationValueBuckets() {
        int[] riskScoreAttenuationValueBuckets = new int[]{
                73,
                63,
                51,
                33,
                27,
                15,
                10,
        };
        return Ints.asList(riskScoreAttenuationValueBuckets);
    }

    /**
     * Minimum estimated time two devices need to be in range of each other before considered a
     * valid exposure.
     */
    public static long minExposureBucketizedDurationSeconds() {
        return 1;
    }

    /**
     * The default minimum attenuation value that must be reached within an exposure duration.
     */
    public static int defaultMinExposureAttenuationValue() {
        return 47; // ~2m distance
    }

    /**
     * Max threshold duration between scans to be considered continuous.
     * <p>
     * Note that this duration is not bucketized by scan interval, and is strictly
     * measured against the duration between two consecutive ble scans.
     */
    public static int maxInterpolationDurationSeconds() {
        // Set to be a little larger than scan interval,
        // which when the phone is in idle mode, can be up
        // to 9 minutes despite scan interval's value, to
        // make sure consecutive scans are not considered
        // individual encounters.
        return 10 * 60;
    }

    /**
     * Maximum duration (in minutes) of ExposureWindow.
     */
    public static int maxExposureWindowDurationMinutes() {
        return 30;
    }

    /**
     * Maximum minutes since last scan in ExposureWindow.
     */
    public static int maxMinutesSinceLastScan() {
        return 5;
    }

    /**
     * Default minutes since last scan in ExposureWindow, used when previous scan time is unknown.
     */
    public static int defaultMinutesSinceLastScan() {
        return 2;
    }

    /**
     * When true, clock drift (embargo) leeway will not be considered when matching near the end
     * of a key's validity period.
     */
    public static boolean ignoreEmbargoPeriodWhenMatchingNearKeyEdges() {
        return false;
    }

    /**
     * Whether to use native code to do matching
     */
    public static boolean matchingWithNative() {
        return true;
    }

    /**
     * Pre processing to filter non-matched IDs, then do the matching.
     */
    public static boolean useMatchingPreFilter() {
        return true;
    }

    /**
     * Whether to allow RECURSIVE report type in TEK.
     */
    public static boolean enableRecursiveTekReportType() {
        return false;
    }

    /**
     * Print more log for matching for debugging
     */
    public static boolean moreLogForMatching() {
        return false;
    }

    /**
     * Whether sightings of the same RPI in a single scan instance should be aggregated and only
     * min attenuation used.
     */
    public static boolean aggregateSightingsFromSingleScan() {
        return true;
    }

    /**
     * Maximum number of keys that will be passed to native for matching in one native matching call
     */
    public static int matchingWithNativeBufferKeySize() {
        return 10000;
    }

    /**
     * If enabled, will start supporting revocation and change of status for report type.
     */
    public static boolean supportRevocationAndChangeStatusReportType() {
        return true;
    }

    /**
     * If enabled, revoked key matches will be stored in the match database (but only when
     * using ExposureWindows).
     */
    public static boolean storeMatchesForRevokedKeys() {
        return true;
    }

    /**
     * Keep existing matching logic, only add a quick filter for non-matched IDs (which should be
     * 99.9% cases).
     */
    public static boolean useMatchingFilter() {
        return true;
    }

    /**
     * Matched RPIs with a TX power higher than this bound will be rejected as outside reasonable
     * range and potentially malicious.
     */
    public static int matchingTxPowerUpperBound() {
        return 20; // dB
    }

    /**
     * Matched RPIs with a TX power lower than this bound will be rejected as outside reasonable
     * range and potentially malicious.
     */
    public static int matchingTxPowerLowerBound() {
        return -50; // dB
    }

    /**
     * Whether the native matching should use the native key file parser
     */
    public static boolean useNativeKeyParser() {
        return true;
    }

    /**
     * If enabled, all exposure results will be written at the end of matching instead of
     * intermittedly throughout the process.
     */
    public static boolean storeExposureResultsInTransaction() {
        return true;
    }

    /**
     * The number of times Report Type can be changed.
     */
    public static int allowedReportTypeTransitions() {
        return 1;
    }

    /**
     * Whether to enable the compact storage format for contact record
     * data store (sightings storage). Instead storing all information for
     * individual received BLE packet, compact format aggregates some fields
     * while keeping other required packet-specific information unaggregated.
     */
    public static boolean contactRecordStoreCompactFormatEnabled() {
        return false;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import com.google.samples.exposurenotification.storage.ContactRecordDataStore;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

/**
 * Pre-processes the local scanned IDs
 */
public class ContactRecordLookUpTable {
    private static final int INDEX_TABLE_SIZE = 65536;
    private final List<byte[]> sortedIds;
    private final int[] indexTable;

    private ContactRecordLookUpTable(List<byte[]> sortedIds, int[] indexTable) {
        this.sortedIds = sortedIds;
        this.indexTable = indexTable;
    }

    public static ContactRecordLookUpTable create(ContactRecordDataStore contactRecordDataStore) {
        List<byte[]> sortedIds = sortIds(contactRecordDataStore);
        return new ContactRecordLookUpTable(sortedIds, indexing(sortedIds));
    }

    public static ContactRecordLookUpTable createDefault() {
        return new ContactRecordLookUpTable(new ArrayList<>(), new int[0]);
    }

    private static List<byte[]> sortIds(ContactRecordDataStore contactRecordDataStore) {
        List<byte[]> ids = contactRecordDataStore.getAllRawIds();
        Collections.sort(
                ids,
                new Comparator<byte[]>() {
                    @Override
                    public int compare(byte[] lhs, byte[] rhs) {
                        // Compares first 2-byte, 1 - greater than, 0 - equal, -1 - less than
                        return Integer.compare(idToIndexValue(lhs), idToIndexValue(rhs));
                    }
                });
        return ids;
    }

    private static int[] indexing(List<byte[]> sortedIds) {
        int[] indexTable = new int[INDEX_TABLE_SIZE]; // 2-byte prefix as the index of the table.
        int lastIndex = 0;
        for (int i = 0; i < sortedIds.size(); i++) {
            int currentIndex = idToIndexValue(sortedIds.get(i));
            while (lastIndex < currentIndex) {
                indexTable[lastIndex++] = i;
            }
        }
        while (lastIndex < INDEX_TABLE_SIZE) {
            indexTable[lastIndex++] = sortedIds.size();
        }
        return indexTable;
    }

    private static int idToIndexValue(byte[] id) {
        return ((id[0] & 0xff) << 8) | (id[1] & 0xff);
    }

    public boolean find(byte[] id) {
        int index = idToIndexValue(id);
        int startIndex = (index > 0) ? indexTable[index - 1] : 0;
        int endIndex = indexTable[index];
        for (; startIndex < endIndex; startIndex++) {
            if (Arrays.equals(id, sortedIds.get(startIndex))) {
                return true;
            }
        }
        return false;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import com.google.samples.exposurenotification.nearby.DailySummariesConfig;
import com.google.samples.exposurenotification.nearby.DailySummariesConfig.DistanceEstimate;
import com.google.samples.exposurenotification.nearby.DailySummary;
import com.google.samples.exposurenotification.nearby.DailySummary.DailySummaryBuilder;
import com.google.samples.exposurenotification.nearby.DailySummary.ExposureSummaryData;
import com.google.samples.exposurenotification.nearby.DailySummary.ExposureSummaryData.ExposureSummaryDataBuilder;
import com.google.samples.exposurenotification.ExposureKeyExportProto;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.TekMetadataUtils;
import com.google.samples.exposurenotification.storage.ExposureWindowProto;
import com.google.samples.exposurenotification.storage.ScanInstanceProto;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.google.samples.exposurenotification.matching.ExposureWindowUtils.getTypicalAttenuation;
import static java.lang.Math.max;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Utilities for creating daily summaries.
 */
public class DailySummaryUtils {
    public static List<DailySummary> getDailySummaries(
            DailySummariesConfig config, List<ExposureWindowProto> exposureWindows) {
        Map<Long, List<ExposureWindowProto>> windows = new HashMap<>();
        for (ExposureWindowProto window : exposureWindows) {
            if (!TekMetadataUtils.isTekReportTypeReturnableToClients(window.getTekMetadata())) {
                Log.log
                        .atInfo()
                        .log(
                                "ExposureWindow ignored due to invalid report type %s",
                                window.getTekMetadata().getReportType());
                continue;
            }
            long windowEpochDays = SECONDS.toDays(window.getEpochSeconds());
            if (config.getDaysSinceExposureThreshold() > 0) {
                long todayEpochDays = MILLISECONDS.toDays(System.currentTimeMillis());
                if (todayEpochDays - windowEpochDays > config.getDaysSinceExposureThreshold()) {
                    Log.log
                            .atInfo()
                            .log(
                                    "ExposureWindow ignored due to too old, days since Window.Days=%d,"
                                            + " Config.daysSinceExposureThreshold=%d",
                                    todayEpochDays - windowEpochDays, config.getDaysSinceExposureThreshold());
                    continue;
                }
            }

            List<ExposureWindowProto> windowProtoList = windows.get(windowEpochDays);
            if (windowProtoList == null) {
                windowProtoList = new ArrayList<>();
                windows.put(windowEpochDays, windowProtoList);
            }
            windowProtoList.add(window);
        }

        if (windows.isEmpty()) {
            return new ArrayList<>();
        }

        ArrayList<DailySummary> result = new ArrayList<>(windows.size());
        for (long epochDay : windows.keySet()) {
            List<ExposureWindowProto> windowProtoList = windows.get(epochDay);
            SummaryItemScores[] scoresOfAllReports =
                    new SummaryItemScores
                            [ExposureKeyExportProto.TemporaryExposureKey.ReportType.values().length];
            for (int i = 0; i < scoresOfAllReports.length; i++) {
                scoresOfAllReports[i] = new SummaryItemScores();
            }

            for (ExposureWindowProto exposureWindowProto : windowProtoList) {
                SummaryItemScores scores =
                        scoresOfAllReports[exposureWindowProto.getTekMetadata().getReportType().getNumber()];
                double windowWeightedDuration =
                        calculateWindowWeightedDuration(exposureWindowProto, config);
                double windowScore =
                        calculateWindowScore(exposureWindowProto, windowWeightedDuration, config);
                if (windowScore < config.getMinimumWindowScore()) {
                    Log.log
                            .atInfo()
                            .log(
                                    "ExposureWindow(%d) ignored due to score(%.3f) is lower than"
                                            + " MinimumWindowScore(%.3f)",
                                    exposureWindowProto.getEpochSeconds(),
                                    windowScore,
                                    config.getMinimumWindowScore());
                    continue;
                }
                scores.sum += windowScore;
                scores.maximum = max(scores.maximum, windowScore);
                scores.weightedDurationSum += windowWeightedDuration;
            }

            result.add(createDailySummary((int) epochDay, scoresOfAllReports));
        }
        return result;
    }

    private static double calculateWindowWeightedDuration(
            ExposureWindowProto window, DailySummariesConfig config) {
        int[] durations = new int[DailySummariesConfig.DistanceEstimate.values().length];

        for (ScanInstanceProto scan : window.getScanInstancesList()) {
            durations[
                    DistanceEstimate.getDistanceEstimate(getTypicalAttenuation(scan), config)
                            .ordinal()] +=
                    scan.getSecondsSinceLastScan();
        }
        double result = 0;
        for (DistanceEstimate distanceEstimate : DistanceEstimate.values()) {
            result +=
                    durations[distanceEstimate.ordinal()]
                            * config.getAttenuationBucketWeights().get(distanceEstimate.ordinal());
        }
        return result;
    }

    private static class SummaryItemScores {
        private double maximum = 0;
        private double sum = 0;
        private double weightedDurationSum = 0;
    }

    private static DailySummary createDailySummary(
            int epochDays, SummaryItemScores[] scoresOfAllReports) {
        DailySummaryBuilder dailySummaryBuilder = new DailySummaryBuilder();
        dailySummaryBuilder.setDaysSinceEpoch(epochDays);
        for (ExposureKeyExportProto.TemporaryExposureKey.ReportType reportType :
                ExposureKeyExportProto.TemporaryExposureKey.ReportType.values()) {
            dailySummaryBuilder.setReportSummary(
                    reportType.getNumber(),
                    createExposureSummaryData(scoresOfAllReports[reportType.getNumber()]));
        }

        SummaryItemScores scoresOfDaySummary = new SummaryItemScores();
        for (ExposureKeyExportProto.TemporaryExposureKey.ReportType reportType :
                ExposureKeyExportProto.TemporaryExposureKey.ReportType.values()) {
            SummaryItemScores scoreOfReport = scoresOfAllReports[reportType.getNumber()];
            scoresOfDaySummary.maximum = max(scoresOfDaySummary.maximum, scoreOfReport.maximum);
            scoresOfDaySummary.sum += scoreOfReport.sum;
            scoresOfDaySummary.weightedDurationSum += scoreOfReport.weightedDurationSum;
        }
        dailySummaryBuilder.setSummaryData(createExposureSummaryData(scoresOfDaySummary));
        return dailySummaryBuilder.build();
    }

    private static ExposureSummaryData createExposureSummaryData(SummaryItemScores scores) {
        return new ExposureSummaryDataBuilder()
                .setScoreSum(scores.sum)
                .setMaximumScore(scores.maximum)
                .setWeightedDurationSum(scores.weightedDurationSum)
                .build();
    }

    private static double calculateWindowScore(
            ExposureWindowProto window, double windowWeightedDuration, DailySummariesConfig config) {
        return windowWeightedDuration
                * config.getReportTypeWeights().get(window.getTekMetadata().getReportType().getNumber())
                * config
                .getInfectiousnessWeights()
                .get(window.getTekMetadata().getInfectiousness().getNumber());
    }

    private DailySummaryUtils() {
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Pair;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.google.common.collect.ImmutableList;
import com.google.samples.Clock;
import com.google.samples.Clock.DefaultClock;
import com.google.samples.Hex;
import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType;
import com.google.samples.exposurenotification.ExposureNotificationEnums.Infectiousness;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.ble.utils.Constants;
import com.google.samples.exposurenotification.crypto.AesEcbEncryptor;
import com.google.samples.exposurenotification.crypto.CryptoException;
import com.google.samples.exposurenotification.data.AssociatedEncryptedMetadata;
import com.google.samples.exposurenotification.data.BluetoothMetadata;
import com.google.samples.exposurenotification.data.DayNumber;
import com.google.samples.exposurenotification.data.GeneratedRollingProximityId;
import com.google.samples.exposurenotification.data.RollingProximityId;
import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;
import com.google.samples.exposurenotification.data.generator.AssociatedEncryptedMetadataGenerator;
import com.google.samples.exposurenotification.data.generator.RollingProximityIdGenerator;
import com.google.samples.exposurenotification.features.ContactTracingFeature;
import com.google.samples.exposurenotification.storage.CloseableIterable;
import com.google.samples.exposurenotification.storage.CompletedMatchingRequestRecord;
import com.google.samples.exposurenotification.storage.ContactRecordDataStore;
import com.google.samples.exposurenotification.storage.ContactRecordDataStore.ContactRecord;
import com.google.samples.exposurenotification.storage.ExposureResult;
import com.google.samples.exposurenotification.storage.ExposureResultStorage;
import com.google.samples.exposurenotification.storage.ExposureWindowProto;
import com.google.samples.exposurenotification.storage.SightingRecord;
import com.google.samples.exposurenotification.storage.StorageException;
import com.google.samples.exposurenotification.storage.TekMetadataRecord;

import org.joda.time.Duration;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import static com.google.samples.exposurenotification.ExposureNotificationEnums.Infectiousness.INFECTIOUSNESS_STANDARD;
import static com.google.samples.exposurenotification.ble.data.RollingProximityIdValidator.getValidWindowEndIntervalNumber;
import static com.google.samples.exposurenotification.ble.data.RollingProximityIdValidator.getValidWindowStartIntervalNumber;
import static com.google.samples.exposurenotification.ble.data.RollingProximityIdValidator.isSightingValid;
import static com.google.samples.exposurenotification.data.TemporaryExposureKeySupport.getRollingStartIntervalNumber;
import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * Given a {@link MatchingRequest}, generates {@link ExposureResult}s from sightings seen.
 *
 * <p>This class loads {@link TemporaryExposureKey}s from the provided {@link MatchingRequest} and
 * runs matching against the {@link ContactRecordDataStore}. The following evaluations are done as
 * part of matching:
 *
 * <ul>
 *   <li>Checking sighted {@link RollingProximityId}s are seen in specified interval number or
 *       within {@link ContactTracingFeature#tkMatchingClockDriftRollingPeriods()} of either side of
 *       the interval.
 *   <li>Filtering exposure to be at least {@link TracingParams#minExposureBucketizedDuration()}.
 *   <li>Bucketing exposures to be multiples of five minutes. Implementation the previous point and
 *       this one is implemented in {@link KeyExposureEvaluator}.
 *   <li>Risk score calculation using {@link RiskScoreCalculator}.
 * </ul>
 *
 * <p>An exposure or encounter is a series of consecutive sightings of {@link RollingProximityId}s
 * generated by the same diagnosed {@link TemporaryExposureKey}.
 */
public class ExposureMatchingTracer {
    @Deprecated
    public static final String TOKEN_A = "ATOKEN";

    static final Comparator<SightingRecordWithMetadata> BY_TIME_ASCENDING =
            (sr1, sr2) -> sr1.sightingRecord().getEpochSeconds() - sr2.sightingRecord().getEpochSeconds();
    private static final SimpleDateFormat dataFormat =
            new SimpleDateFormat("MM-dd HH:mm:ss", Locale.ENGLISH);

    private final Context context;
    private final MatchingRequest matchingRequest;
    private final String instanceLogTag;
    private final TekMetadataRecord tekMetadataRecord;
    private final KeyExposureEvaluator keyExposureEvaluator;
    private volatile boolean stopRequested = false;

    @VisibleForTesting
    int diagnosisKeyCount;
    @VisibleForTesting
    int processedKeysCount;
    @VisibleForTesting
    int matchesFoundCount;

    public ExposureMatchingTracer(Context context, MatchingRequest matchingRequest) {
        this(context, matchingRequest, DefaultClock.getInstance());
    }

    @VisibleForTesting
    public ExposureMatchingTracer(
            Context context,
            MatchingRequest matchingRequest,
            Clock clock) {
        this.context = context;
        this.matchingRequest = matchingRequest;
        this.diagnosisKeyCount = 0;
        this.processedKeysCount = 0;
        this.matchesFoundCount = 0;
        this.instanceLogTag = getTag(matchingRequest);
        this.tekMetadataRecord =
                getTekMetadataRecord(
                        context, matchingRequest.packageName(), matchingRequest.signatureHash());
        this.keyExposureEvaluator =
                new KeyExposureEvaluator(
                        matchingRequest.exposureConfiguration(),
                        RiskScoreCalculator.create(clock),
                        TracingParams.builder().build(),
                        instanceLogTag,
                        tekMetadataRecord);
    }

    private String getTag(MatchingRequest matchingRequest) {
        return String.format("Matching#%04x", matchingRequest.hashCode());
    }

    /**
     * If there are still keys to be processed in {@link #trace()}, this method will stop tracing
     * after the {@link TemporaryExposureKey} that is currently being matched.
     */
    public void stopTrace() {
        stopRequested = true;
    }

    /**
     * Returns the request associated with the tracer.
     */
    public MatchingRequest getMatchingRequest() {
        return matchingRequest;
    }

    /**
     * Finds exposures to the provided {@link TemporaryExposureKey}s and stores the records of
     * exposure based on the {@link TracingParams} provided. Returns true if matches were found, false
     * otherwise.
     *
     * <p>See class description for details on what an exposure means.
     */
    public boolean trace() throws StorageException, CryptoException {
        boolean matchesFound;
        if (ContactTracingFeature.matchingWithNative()) {
            matchesFound = traceWithNative();
        } else {
            if (ContactTracingFeature.useMatchingPreFilter()) {
                matchesFound = traceWithPreFilter();
            } else {
                try (CloseableIterable<TemporaryExposureKey> temporaryExposureKeys =
                             matchingRequest.diagnosisKeys()) {
                    matchesFound = traceWithJava(temporaryExposureKeys);
                } catch (IOException e) {
                    Log.log.atSevere().withCause(e).log("Failed to read diagnosis keys");
                    matchesFound = false;
                }
            }
        }
        persistMatchingRequestRecord();
        return matchesFound;
    }

    private void persistMatchingRequestRecord() {
        Log.log.atInfo().log("%s Persisting matching request record.", instanceLogTag);
        CompletedMatchingRequestRecord completedMatchingRequestRecord =
                CompletedMatchingRequestRecord.newBuilder()
                        .setTemporaryKeysBatchHash(matchingRequest.keyFilesHash())
                        .setTimestampMillis(System.currentTimeMillis())
                        .setKeyCount(diagnosisKeyCount > 0 ? diagnosisKeyCount : processedKeysCount)
                        .setMatchesCount(matchesFoundCount)
                        .build();
        // FIXME: Record the completed matching request.
    }

    private boolean traceWithNative() throws StorageException, CryptoException {
        Log.log.atInfo().log("%s Native pre-filter started.", instanceLogTag);
        long startTime = System.currentTimeMillis();
        try (ContactRecordDataStore contactRecordDataStore = ContactRecordDataStore.open(context)) {
            List<byte[]> idList = contactRecordDataStore.getAllRawIds();
            try (MatchingJni matchingJni = new MatchingJni(context, idList.toArray(new byte[0][]))) {
                Set<TemporaryExposureKey> matchedKeyList;
                if (ContactTracingFeature.useNativeKeyParser()) {
                    matchedKeyList = matchingJni.matching(matchingRequest.diagnosisKeyFiles());
                    diagnosisKeyCount = matchingJni.getLastProcessedKeyCount();
                } else {
                    try (CloseableIterable<TemporaryExposureKey> temporaryExposureKeys =
                                 matchingRequest.diagnosisKeys()) {
                        Pair<Integer, Set<TemporaryExposureKey>> keysCountPair =
                                matchingJni.matchingLegacy(temporaryExposureKeys);
                        diagnosisKeyCount = keysCountPair.first;
                        matchedKeyList = keysCountPair.second;
                    } catch (IOException e) {
                        Log.log.atSevere().withCause(e).log("%s Failed to read diagnosis keys", instanceLogTag);
                        return false;
                    }
                }
                Log.log
                        .atInfo()
                        .log(
                                "%s Native pre-filter found %d (%d) keys with sightings out of %d scan"
                                        + " records. Spent time: %.3fs",
                                instanceLogTag,
                                matchedKeyList.size(),
                                diagnosisKeyCount,
                                idList.size(),
                                (System.currentTimeMillis() - startTime) / 1000f);
                return traceWithJava(matchedKeyList);
            }
        }
    }

    private boolean traceWithPreFilter() {
        Log.log.atInfo().log("%s Java pre-filter started.", instanceLogTag);
        try (ContactRecordDataStore contactRecordDataStore = ContactRecordDataStore.open(context);
             CloseableIterable<TemporaryExposureKey> temporaryExposureKeys =
                     matchingRequest.diagnosisKeys()) {
            RollingProximityIdGenerator.Factory idGeneratorFactory =
                    new RollingProximityIdGenerator.Factory();
            List<TemporaryExposureKey> matchedKeyList = new ArrayList<>();
            ContactRecordLookUpTable contactRecordLookUpTable =
                    ContactRecordLookUpTable.create(contactRecordDataStore);
            byte[] reusedEncryptedOutput =
                    new byte
                            [(int)
                            (ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod()
                                    * ContactTracingFeature.contactIdLength())]; // byte[144 * 16] = byte[2304].
            AesEcbEncryptor aesEcbEncryptor = AesEcbEncryptor.create();
            for (TemporaryExposureKey diagnosisKey : temporaryExposureKeys) {
                diagnosisKeyCount++;
                List<GeneratedRollingProximityId> rollingProximityIds;
                // We prefilter based on all the possible RPIs to allow saving of attempted keys in the
                // actual matching.
                rollingProximityIds =
                        idGeneratorFactory
                                .getInstance(
                                        aesEcbEncryptor,
                                        diagnosisKey.getKeyData(),
                                        diagnosisKey.getRollingStartIntervalNumber(),
                                        TemporaryExposureKeySupport.getMaxPossibleRollingEndIntervalNumber(
                                                diagnosisKey),
                                        (int) ContactTracingFeature.rollingProximityIdKeySizeBytes(),
                                        ContactTracingFeature.rpikHkdfInfoString(),
                                        ContactTracingFeature.rpidAesPaddedString())
                                .generateIds(reusedEncryptedOutput);
                for (GeneratedRollingProximityId rollingProximityId : rollingProximityIds) {
                    if (contactRecordLookUpTable.find(rollingProximityId.rollingProximityId().getDirect())) {
                        matchedKeyList.add(diagnosisKey);
                        break;
                    }
                }
            }

            Log.log
                    .atInfo()
                    .log(
                            "%s Java pre-filter found %d keys with sightings out of %d total diagnosis keys",
                            instanceLogTag, matchedKeyList.size(), diagnosisKeyCount);
            return traceWithJava(matchedKeyList);
        } catch (StorageException | CryptoException | IOException e) {
            Log.log
                    .atWarning()
                    .withCause(e)
                    .log("%s Failure while running matching with java pre-filter", instanceLogTag);
        }

        return false;
    }

    @SuppressLint("WrongConstant")
    private boolean traceWithJava(Iterable<TemporaryExposureKey> diagnosisKeys)
            throws StorageException, CryptoException {
        Log.log.atInfo().log("%s Java tracing started.", instanceLogTag);
        if (ContactTracingFeature.moreLogForMatching()) {
            int keyCount = 0;
            Log.log.atInfo().log("Dump TEKs for matching");
            for (TemporaryExposureKey temporaryExposureKey : diagnosisKeys) {
                Log.log
                        .atInfo()
                        .log(
                                "%s, %d. Rolling start time:%s, rolling period:%d",
                                Constants.fromIdByteArrayToString(temporaryExposureKey.getKeyData()),
                                temporaryExposureKey.getRollingStartIntervalNumber(),
                                fromIntervalNumberToDateString(
                                        temporaryExposureKey.getRollingStartIntervalNumber()),
                                temporaryExposureKey.getRollingPeriod());
                keyCount++;
                if (keyCount > 10) {
                    Log.log.atInfo().log("Stop dumping TEKs due to meet maximum dumping size.");
                    break;
                }
            }
        }
        RollingProximityIdGenerator.Factory idGeneratorFactory =
                new RollingProximityIdGenerator.Factory();
        AssociatedEncryptedMetadataGenerator.Factory metadataGeneratorFactory =
                new AssociatedEncryptedMetadataGenerator.Factory();
        boolean foundMatches = false;
        stopRequested = false;
        try (ContactRecordDataStore contactRecordDataStore = ContactRecordDataStore.open(context);
             ExposureResultStorage exposureResultStore = ExposureResultStorage.open(context);
             AttemptedKeysDataStore attemptedKeysDataStore = AttemptedKeysDataStore.open(context)) {
            AesEcbEncryptor aesEcbEncryptor = AesEcbEncryptor.create();
            byte[] reusedEncryptedOutput =
                    new byte
                            [(int)
                            (ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod()
                                    * ContactTracingFeature.contactIdLength())]; // byte[144 * 16] = byte[2304].
            ContactRecordLookUpTable contactRecordLookUpTable =
                    ContactTracingFeature.useMatchingFilter()
                            ? ContactRecordLookUpTable.create(contactRecordDataStore)
                            : ContactRecordLookUpTable.createDefault();
            byte[] tokenRoot =
                    ExposureResultStorage.encodeTokenRoot(
                            matchingRequest.packageName(),
                            matchingRequest.signatureHash(),
                            matchingRequest.token());
            byte[] packageRoot =
                    AttemptedKeysDataStore.encodePackageRoot(
                            matchingRequest.packageName(), matchingRequest.signatureHash());
            for (TemporaryExposureKey diagnosisKey : diagnosisKeys) {
                if (stopRequested) {
                    Log.log
                            .atInfo()
                            .log(
                                    "%s Matching pre-empted. Processed %d keys.", instanceLogTag, processedKeysCount);
                    return foundMatches;
                }
                ++processedKeysCount;

                if (exposureResultStore.hasResult(tokenRoot, diagnosisKey.getKeyData())) {
                    if (ContactTracingFeature.supportRevocationAndChangeStatusReportType()
                            && storeValidReportTransition(
                            exposureResultStore,
                            tokenRoot,
                            diagnosisKey,
                            tekMetadataRecord.getReportTypeWhenMissing())) {
                        Log.log
                                .atInfo()
                                .log(
                                        "%s Updated report type of matched diagnosis key because there was a valid"
                                                + " transition on the previous match.",
                                        instanceLogTag);
                    } else {
                        Log.log
                                .atInfo()
                                .log(
                                        "%s Skipping a diagnosis key because there was a previous match.",
                                        instanceLogTag);
                        Log.log
                                .atVerbose()
                                .log("%s -- Diagnosis key #%d skipped.", instanceLogTag, processedKeysCount);
                    }
                    continue;
                }
                if (attemptedKeysDataStore.rollingPeriodChanged(
                        packageRoot, diagnosisKey.getKeyData(), diagnosisKey.getRollingPeriod())) {
                    // Do not match keys that were already attempted and rolling period has changed. Rolling
                    // period should never change for a key. This is kept on a per app but NOT per-token
                    // basis.
                    Log.log
                            .atWarning()
                            .log(
                                    "%s Attempted to match TEK with a changed rolling period, skipping.",
                                    instanceLogTag);
                    continue;
                }
                if (!ContactTracingFeature.enableRecursiveTekReportType()
                        && diagnosisKey.getReportType() == ReportType.RECURSIVE_VALUE) {
                    Log.log
                            .atWarning()
                            .log(
                                    "%s Attempted to match TEK with RECURSIVE report type which is not enabled.",
                                    instanceLogTag);
                    continue;
                }
                if (diagnosisKey.getReportType() >= ReportType.REVOKED_VALUE) {
                    if (matchingRequest.token().equals(TOKEN_A)
                            && diagnosisKey.getReportType() == ReportType.REVOKED_VALUE
                            && ContactTracingFeature.storeMatchesForRevokedKeys()) {
                        Log.log.atInfo().log("%s Attemping to match TEK with revoked type.", instanceLogTag);
                    } else {
                        Log.log
                                .atWarning()
                                .log("%s Attempted to match TEK with report type >= REVOKED.", instanceLogTag);
                        continue;
                    }
                }

                // Generates all possibly valid RPIs for this key.
                List<GeneratedRollingProximityId> rollingProximityIds =
                        idGeneratorFactory
                                .getInstance(
                                        aesEcbEncryptor,
                                        diagnosisKey.getKeyData(),
                                        diagnosisKey.getRollingStartIntervalNumber(),
                                        TemporaryExposureKeySupport.getMaxPossibleRollingEndIntervalNumber(
                                                diagnosisKey),
                                        (int) ContactTracingFeature.rollingProximityIdKeySizeBytes(),
                                        ContactTracingFeature.rpikHkdfInfoString(),
                                        ContactTracingFeature.rpidAesPaddedString())
                                .generateIds(reusedEncryptedOutput);

                TemporaryExposureKey diagnosisKeyIgnoringRollingPeriod =
                        new TemporaryExposureKey.TemporaryExposureKeyBuilder()
                                .setKeyData(diagnosisKey.getKeyData())
                                .setRollingStartIntervalNumber(diagnosisKey.getRollingStartIntervalNumber())
                                .setRollingPeriod(
                                        (int) ContactTracingFeature.tkRollingPeriodMultipleOfIdRollingPeriod())
                                .setTransmissionRiskLevel(diagnosisKey.getTransmissionRiskLevel())
                                .setReportType(diagnosisKey.getReportType())
                                .build();
                Log.log.atInfo().log("%s Matching scans for all possible RPIs.", instanceLogTag);
                List<SightingRecordWithMetadata> matchingScansForAllPossibleRpis =
                        fetchValidSightings(
                                instanceLogTag,
                                metadataGeneratorFactory,
                                contactRecordDataStore,
                                contactRecordLookUpTable,
                                rollingProximityIds,
                                diagnosisKeyIgnoringRollingPeriod,
                                /*aggregateSightings=*/ false);

                if (!matchingScansForAllPossibleRpis.isEmpty()) {
                    // Key could possibly match to some RPIs, saving the rolling period with which it was
                    // attempted.
                    attemptedKeysDataStore.storeAttemptedKeyRollingPeriod(packageRoot, diagnosisKey);
                }

                Log.log
                        .atInfo()
                        .log(
                                "%s Matching scans for RPIs matching rolling period %d.",
                                instanceLogTag, diagnosisKey.getRollingPeriod());
                List<SightingRecordWithMetadata> matchingScansWithNoAggregation =
                        fetchValidSightings(
                                instanceLogTag,
                                metadataGeneratorFactory,
                                contactRecordDataStore,
                                contactRecordLookUpTable,
                                rollingProximityIds.subList(0, diagnosisKey.getRollingPeriod()),
                                diagnosisKey,
                                /*aggregateSightings=*/ false);

                List<SightingRecordWithMetadata> matchingScans;
                if (ContactTracingFeature.aggregateSightingsFromSingleScan()) {
                    matchingScans =
                            fetchValidSightings(
                                    instanceLogTag,
                                    metadataGeneratorFactory,
                                    contactRecordDataStore,
                                    contactRecordLookUpTable,
                                    rollingProximityIds.subList(0, diagnosisKey.getRollingPeriod()),
                                    diagnosisKey,
                                    /*aggregateSightings=*/ ContactTracingFeature.aggregateSightingsFromSingleScan());
                } else {
                    matchingScans = matchingScansWithNoAggregation;
                }
                ExposureResult exposureResult =
                        keyExposureEvaluator.findExposures(diagnosisKey, matchingScans);

                Log.log
                        .atInfo()
                        .log(
                                "%s Checked matches for diagnosis key #%d against %d matching scans.",
                                instanceLogTag, processedKeysCount, matchingScans.size());
                if (exposureResult == null) {
                    continue;
                }
                List<ExposureWindowProto> exposureWindows;
                if (matchingRequest.token().equals(TOKEN_A)) {
                    exposureWindows =
                            keyExposureEvaluator.findExposureWindows(
                                    diagnosisKey, matchingScansWithNoAggregation);
                    exposureResult =
                            exposureResult.toBuilder().addAllExposureWindows(exposureWindows).build();
                }
                Log.log.atInfo().log("%s Match found for a diagnosis key.", instanceLogTag);
                Log.log
                        .atVerbose()
                        .log("%s -- Diagnosis key #%d matched.", instanceLogTag, processedKeysCount);
                foundMatches = true;
                matchesFoundCount++;
                exposureResultStore.storeResult(
                        tokenRoot,
                        diagnosisKey,
                        exposureResult,
                        ContactTracingFeature.storeExposureResultsInTransaction());
            }
            if (ContactTracingFeature.storeExposureResultsInTransaction()) {
                exposureResultStore.commitStoreResultRequestsForTransaction();
            }
        }
        Log.log
                .atInfo()
                .log(
                        "%s Traced %d diagnosis keys and found %d matches.",
                        instanceLogTag, processedKeysCount, matchesFoundCount);
        return foundMatches;
    }

    @VisibleForTesting
    static boolean storeValidReportTransition(
            ExposureResultStorage exposureResultStore,
            byte[] tokenRoot,
            TemporaryExposureKey diagnosisKey,
            ReportType reportTypeWhenMissing) {
        ReportType newType =
                ExposureWindowUtils.getReportType(diagnosisKey.getReportType(), reportTypeWhenMissing);
        ExposureResult previousResult =
                exposureResultStore.getResult(tokenRoot, diagnosisKey.getKeyData());
        boolean validTransition =
                isValidTransition(previousResult.getTekMetadata().getReportType(), newType);
        if (validTransition
                && previousResult.getReportTypeTransitionCount()
                < ContactTracingFeature.allowedReportTypeTransitions()) {
            List<ExposureWindowProto> updatedWindows = new ArrayList<>();
            for (ExposureWindowProto window : previousResult.getExposureWindowsList()) {
                updatedWindows.add(
                        window.toBuilder()
                                .setTekMetadata(window.getTekMetadata().toBuilder().setReportType(newType).build())
                                .build());
            }

            ExposureResult updatedExposureResult =
                    previousResult.toBuilder()
                            .setTekMetadata(previousResult.getTekMetadata().toBuilder().setReportType(newType))
                            .setReportTypeTransitionCount(previousResult.getReportTypeTransitionCount() + 1)
                            .clearExposureWindows()
                            .addAllExposureWindows(updatedWindows)
                            .build();

            return exposureResultStore.storeResult(
                    tokenRoot, diagnosisKey, updatedExposureResult, /*holdForTransaction=*/ false);
        }
        return false;
    }

    /**
     * Returns sorted sightings with metadata for the given list of generated ids.
     */
    static List<SightingRecordWithMetadata> fetchValidSightings(
            String instanceLogTag,
            AssociatedEncryptedMetadataGenerator.Factory metadataGeneratorFactory,
            ContactRecordDataStore contactRecordDataStore,
            @Nullable ContactRecordLookUpTable recordPreprocessor,
            List<GeneratedRollingProximityId> generatedIds,
            TemporaryExposureKey diagnosisKey,
            boolean aggregateSightings)
            throws CryptoException {
        AssociatedEncryptedMetadataGenerator aemGenerator =
                metadataGeneratorFactory.getInstance(diagnosisKey);
        List<SightingRecordWithMetadata> sightingRecordsWithMetadata = new ArrayList<>();
        for (GeneratedRollingProximityId generatedId : generatedIds) {
            if (ContactTracingFeature.useMatchingFilter()
                    && !recordPreprocessor.find(generatedId.rollingProximityId().getDirect())) {
                continue;
            }
            if (ContactTracingFeature.moreLogForMatching()) {
                Log.log
                        .atInfo()
                        .log(
                                "%s TEK %s matches with ID %s:%d",
                                instanceLogTag,
                                Constants.fromIdByteArrayToString(diagnosisKey.getKeyData()),
                                Constants.fromIdByteArrayToString(generatedId.rollingProximityId().getDirect()),
                                generatedId.intervalNumber() - diagnosisKey.getRollingStartIntervalNumber());
            }
            List<SightingRecord> sightingRecords =
                    getValidSightingsForRpi(
                            instanceLogTag,
                            contactRecordDataStore,
                            diagnosisKey,
                            generatedId,
                            aggregateSightings);
            for (SightingRecord sightingRecord : sightingRecords) {
                // This is not a valid "old format" sighting record, skip it.
                if (!sightingRecord.hasRssi()
                        || !sightingRecord.hasAssociatedEncryptedMetadata()
                        || sightingRecord.getAssociatedEncryptedMetadata().size() != 4) {
                    continue;
                }
                BluetoothMetadata metadata =
                        aemGenerator.decrypt(
                                generatedId.rollingProximityId(),
                                AssociatedEncryptedMetadata.create(
                                        sightingRecord.getAssociatedEncryptedMetadata().toByteArray()));
                Log.log
                        .atInfo()
                        .log(
                                "%s Valid sighting found with TX Power=%s, confidence=%s",
                                instanceLogTag, metadata.txPower(), metadata.calibrationConfidence().name());

                if (metadata.txPower() <= ContactTracingFeature.matchingTxPowerUpperBound()
                        && metadata.txPower() >= ContactTracingFeature.matchingTxPowerLowerBound()) {
                    sightingRecordsWithMetadata.add(
                            SightingRecordWithMetadata.create(sightingRecord, metadata));
                } else {
                    Log.log
                            .atInfo()
                            .log(
                                    "%s TX Power %s is outside reasonable bounds [%s, %s].",
                                    instanceLogTag,
                                    metadata.txPower(),
                                    ContactTracingFeature.matchingTxPowerLowerBound(),
                                    ContactTracingFeature.matchingTxPowerUpperBound());
                }
            }
        }
        Collections.sort(sightingRecordsWithMetadata, BY_TIME_ASCENDING);
        return sightingRecordsWithMetadata;
    }

    /**
     * Fetches all valid sightings for a given {@link GeneratedRollingProximityId}.
     *
     * <p>A {@link RollingProximityId} sighting is only valid if it falls within a time window on
     * either side of {@link GeneratedRollingProximityId#intervalNumber()}. The time window is
     * configured in {@link ContactTracingFeature#tkMatchingClockDriftRollingPeriods()}.
     *
     * <p>Given that {@link ContactRecordDataStore} stores sightings keyed by {@link DayNumber}, and
     * it's sometimes valid to have sightings of a {@link RollingProximityId} across day boundaries,
     * this method contains logic to include all and valid sightings of the specified {@link
     * GeneratedRollingProximityId}.
     *
     * <p>Sightings are truncated to the first sightings within 2 * {@link
     * ContactTracingFeature#idRollingPeriodMinutes}, any sightings beyond that (counting from the
     * first sighting) are discarded as invalid. Legitimate clients should never transmit the RPI for
     * more than 20 minutes, hence sightings of the same RPI that are farther apart than that must be
     * replays.
     */
    private static List<SightingRecord> getValidSightingsForRpi(
            String instanceLogTag,
            ContactRecordDataStore dataStore,
            TemporaryExposureKey diagnosisKey,
            GeneratedRollingProximityId generatedId,
            boolean aggregateSightings) {
        DayNumber diagnosisKeyDayNumber = TemporaryExposureKeySupport.getDayNumber(diagnosisKey);
        List<SightingRecord> sightingRecords = new ArrayList<>();
        if (getValidWindowStartIntervalNumber(generatedId)
                < getRollingStartIntervalNumber(diagnosisKeyDayNumber)) {
            ContactRecord prevDayRecord =
                    dataStore.getRecord(
                            new DayNumber(diagnosisKeyDayNumber.getValue() - 1),
                            generatedId.rollingProximityId());
            if (prevDayRecord != null) {
                sightingRecords.addAll(prevDayRecord.getValue().getSightingRecordsList());
            }
        }
        ContactRecord todayRecord =
                dataStore.getRecord(diagnosisKeyDayNumber, generatedId.rollingProximityId());
        if (todayRecord != null) {
            sightingRecords.addAll(todayRecord.getValue().getSightingRecordsList());
        }
        if (getValidWindowEndIntervalNumber(generatedId, diagnosisKey)
                > getRollingStartIntervalNumber(diagnosisKeyDayNumber) + 1) {
            ContactRecord nextDayRecord =
                    dataStore.getRecord(
                            new DayNumber(diagnosisKeyDayNumber.getValue() + 1),
                            generatedId.rollingProximityId());
            if (nextDayRecord != null) {
                sightingRecords.addAll(nextDayRecord.getValue().getSightingRecordsList());
            }
        }

        // Sort by time ascending.
        Collections.sort(sightingRecords, (sr1, sr2) -> sr1.getEpochSeconds() - sr2.getEpochSeconds());

        if (ContactTracingFeature.moreLogForMatching()) {
            Log.log
                    .atInfo()
                    .log(
                            "%s Dump sighting record for RPI %s, %d. Rolling start time:%s",
                            instanceLogTag,
                            Constants.fromIdByteArrayToString(generatedId.rollingProximityId().getDirect()),
                            generatedId.intervalNumber(),
                            fromIntervalNumberToDateString(generatedId.intervalNumber()));
            for (SightingRecord sightingRecord : sightingRecords) {
                Log.log
                        .atInfo()
                        .log(
                                "%s Sighting time:%s rssi=%d, previous_scan_seconds=%d",
                                instanceLogTag,
                                fromDurationToDateString(
                                        Duration.standardSeconds(sightingRecord.getEpochSeconds())),
                                sightingRecord.getRssi(),
                                sightingRecord.getPreviousScanEpochSeconds());
            }
        }

        // Remove invalid sightings.
        Iterator<SightingRecord> validCheckIterator = sightingRecords.iterator();
        while (validCheckIterator.hasNext()) {
            SightingRecord sightingRecord = validCheckIterator.next();
            if (!isSightingValid(
                    generatedId, Duration.standardSeconds(sightingRecord.getEpochSeconds()), diagnosisKey)) {
                Log.log
                        .atInfo()
                        .log("%s Sighting discarded for being found outside of valid window.", instanceLogTag);
                if (ContactTracingFeature.moreLogForMatching()) {
                    Log.log
                            .atInfo()
                            .log(
                                    "%s Sighting time:%s is not valid.",
                                    instanceLogTag,
                                    fromDurationToDateString(
                                            Duration.standardSeconds(sightingRecord.getEpochSeconds())));
                }
                validCheckIterator.remove();
            }
        }

        // Nothing left to do.
        if (sightingRecords.isEmpty()) {
            return sightingRecords;
        }

        int firstSightingTime = sightingRecords.get(0).getEpochSeconds();
        // We allow for 2 * rotation period for the RPIs to be valid. RPIs outside of this
        // time (as calculated from the first sighting) are discarded.
        Iterator<SightingRecord> iterator = sightingRecords.iterator();
        while (iterator.hasNext()) {
            SightingRecord sightingRecord = iterator.next();
            if (sightingRecord.getEpochSeconds() - firstSightingTime
                    > 2 * MINUTES.toSeconds(ContactTracingFeature.idRollingPeriodMinutes())) {
                Log.log.atInfo().log("%s Sighting discarded for suspected replay.", instanceLogTag);
                if (ContactTracingFeature.moreLogForMatching()) {
                    Log.log
                            .atInfo()
                            .log(
                                    "%s Sighting time:%s discarded for suspected replay.",
                                    instanceLogTag,
                                    fromDurationToDateString(
                                            Duration.standardSeconds(sightingRecord.getEpochSeconds())));
                }
                iterator.remove();
            }
        }

        if (aggregateSightings) {
            Iterator<SightingRecord> aggregateSightingsIterator = sightingRecords.iterator();
            List<SightingRecord> aggregatedSightings = new ArrayList<>();
            int aggregateIndex = 0;
            aggregatedSightings.add(aggregateSightingsIterator.next());
            int previousSeconds = aggregatedSightings.get(0).getEpochSeconds();

            while (aggregateSightingsIterator.hasNext()) {
                SightingRecord currentSighting = aggregateSightingsIterator.next();
                int timeDelta = currentSighting.getEpochSeconds() - previousSeconds;
                if (timeDelta
                        > 1.5
                        * (ContactTracingFeature.scanTimeSeconds()
                        + ContactTracingFeature.scanTimeExtendForProfileInUseSeconds())) {
                    // New scan.
                    aggregateIndex += 1;
                    aggregatedSightings.add(currentSighting);
                }
                if (currentSighting.getRssi() > aggregatedSightings.get(aggregateIndex).getRssi()) {
                    aggregatedSightings.set(aggregateIndex, currentSighting);
                }
                previousSeconds = currentSighting.getEpochSeconds();
            }
            sightingRecords = aggregatedSightings;
        }

        if (ContactTracingFeature.moreLogForMatching()) {
            Log.log.atInfo().log("%s Dump filtered sightings", instanceLogTag);
            for (SightingRecord sightingRecord : sightingRecords) {
                Log.log
                        .atInfo()
                        .log(
                                "%s Sighting time:%s rssi=%d, previous_scan_seconds=%d",
                                instanceLogTag,
                                fromDurationToDateString(
                                        Duration.standardSeconds(sightingRecord.getEpochSeconds())),
                                sightingRecord.getRssi(),
                                sightingRecord.getPreviousScanEpochSeconds());
            }
        }

        return sightingRecords;
    }

    private static String fromIntervalNumberToDateString(int intervalNumber) {
        return fromTimeInMsToDateString(
                MINUTES.toMillis((intervalNumber * ContactTracingFeature.idRollingPeriodMinutes())));
    }

    private static String fromDurationToDateString(Duration duration) {
        return fromTimeInMsToDateString(duration.getMillis());
    }

    private static String fromTimeInMsToDateString(long timeInMs) {
        return dataFormat.format(timeInMs);
    }

    private static TekMetadataRecord getTekMetadataRecord(
            Context context, String packageName, byte[] packageSignature) {
        return new ClientRecordDataStore(context)
                .getTekMetadataRecord(packageName, packageSignature);
    }

    @VisibleForTesting
    static boolean isValidTransition(ReportType oldReportType, ReportType newReportType) {
        if (oldReportType == ReportType.SELF_REPORT) {
            return ImmutableList.of(
                    ReportType.CONFIRMED_TEST,
                    ReportType.CONFIRMED_CLINICAL_DIAGNOSIS,
                    ReportType.REVOKED)
                    .contains(newReportType);
        }

        if (oldReportType == ReportType.CONFIRMED_CLINICAL_DIAGNOSIS) {
            return ImmutableList.of(ReportType.CONFIRMED_TEST, ReportType.REVOKED)
                    .contains(newReportType);
        }

        return false;
    }

    /**
     * Stores attempted keys and their associated rolling period.
     *
     * <p>The store is keyed by PackageNameHash:Signature:TemporaryExposureKeyData.
     */
    // FIXME: Replace with a proper data store.
    private static final class AttemptedKeysDataStore implements AutoCloseable {
        public static AttemptedKeysDataStore open(Context context) throws StorageException {
            return new AttemptedKeysDataStore(context);
        }

        private AttemptedKeysDataStore(Context context) {
        }

        /**
         * Encodes package root into byte array.
         */
        public static byte[] encodePackageRoot(String packageName, byte[] signatureHash) {
            // FIXME: Use a better encoding method
            String packageRoot = String.format("{package:%s,hash:%s}",
                    packageName,
                    Hex.bytesToStringLowercase(signatureHash));
            return packageRoot.getBytes();
        }

        public void storeAttemptedKeyRollingPeriod(byte[] packageRoot, TemporaryExposureKey diagnosisKey) {
            // FIXME: Store the attempted key.
        }

        public boolean rollingPeriodChanged(byte[] packageRoot, byte[] keyData, int rollingPeriod) {
            // FIXME: Check if the rolling period has changed.
            return false;
        }

        @Override
        public void close() {
            /* nothing to do */
        }
    }

    private static final class ClientRecordDataStore implements AutoCloseable {
        private final HashMap<String, TekMetadataRecord> store = new HashMap<>();

        public ClientRecordDataStore(Context context) {
        }

        public TekMetadataRecord getTekMetadataRecord(String packageName, byte[] packageSignature) {
            String packageKey = encodePackage(packageName, packageSignature);
            if (store.containsKey(packageKey)) {
                return store.get(packageKey);
            }
            return createDefaultTekMetadataRecord();
        }

        public static TekMetadataRecord createDefaultTekMetadataRecord() {
            Infectiousness[] infectiousnessesArray = new Infectiousness[29];
            Arrays.fill(infectiousnessesArray, INFECTIOUSNESS_STANDARD);
            return TekMetadataRecord.newBuilder()
                    .addAllDaysSinceOnsetToInfectiousness(Arrays.asList(infectiousnessesArray))
                    .build();
        }

        private String encodePackage(String packageName, byte[] packageSignature) {
            // FIXME: Use a better encoding method
            return String.format("{package:%s,hash:%s}",
                    packageName,
                    Hex.bytesToStringLowercase(packageSignature));
        }

        @Override
        public void close() throws Exception {
            /* nothing to do */
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import android.annotation.TargetApi;
import android.os.Build.VERSION_CODES;

import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType;
import com.google.samples.exposurenotification.ExposureNotificationEnums.Infectiousness;
import com.google.samples.exposurenotification.storage.ExposureResult;
import com.google.samples.exposurenotification.storage.ExposureResultStorage;
import com.google.samples.exposurenotification.storage.ExposureWindowProto;
import com.google.samples.exposurenotification.storage.ScanInstanceProto;
import com.google.samples.exposurenotification.storage.StorageException;

import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Instant;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.google.samples.exposurenotification.nearby.DiagnosisKeysDataMapping.MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET;
import static com.google.samples.exposurenotification.nearby.DiagnosisKeysDataMapping.SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS;
import static com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType.REPORT_TYPE_UNKNOWN;
import static com.google.samples.exposurenotification.matching.ExposureMatchingTracer.TOKEN_A;
import static java.lang.Math.min;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Utilities for get exposure windows.
 */
public class ExposureWindowUtils {
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();

    @TargetApi(VERSION_CODES.KITKAT)
    public static List<ExposureWindowProto> getExposureWindows(
            String callingPackage, byte[] signatureHash, ExposureResultStorage exposureResultStorage) {
        List<ExposureWindowProto> exposureWindows = new ArrayList<>();

        try {
            List<ExposureResult> entries =
                    exposureResultStorage.getAll(callingPackage, signatureHash, TOKEN_A);
            for (ExposureResult exposureResult : entries) {
                exposureWindows.addAll(exposureResult.getExposureWindowsList());
            }
        } catch (StorageException e) {
            throw new RuntimeException("FAILED_DISK_IO", e);
        }
        Collections.shuffle(exposureWindows, SECURE_RANDOM);
        return exposureWindows;
    }

    /**
     * Returns epoch timestamp indicating the beginning of the given timezone day for the provided
     * epoch.
     */
    public static long epochToDay(int epoch, DateTimeZone dateTimeZone) {
        Instant instant = new Instant(SECONDS.toMillis(epoch));
        DateTime date = instant.toDateTime(dateTimeZone).withTime(0, 0, 0, 0);
        return date.getMillis();
    }

    public static int getTypicalAttenuation(ScanInstanceProto scanInstanceProto) {
        double sum = 0;
        for (int attenuation : scanInstanceProto.getAttenuationsList()) {
            sum += attenuation;
        }
        return (int) Math.round(sum / scanInstanceProto.getAttenuationsCount());
    }

    public static int getMinAttenuation(ScanInstanceProto scanInstanceProto) {
        int min = Integer.MAX_VALUE;
        for (int attenuation : scanInstanceProto.getAttenuationsList()) {
            min = min(min, attenuation);
        }
        return min;
    }

    public static Infectiousness getInfectiousness(
            int daysSinceOnsetOfSymptoms,
            List<Infectiousness> daysSinceOnsetToInfectiousness,
            Infectiousness defaultInfectiousness) {
        if (Math.abs(daysSinceOnsetOfSymptoms) > MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET) {
            return defaultInfectiousness;
        }
        if (daysSinceOnsetToInfectiousness.size() != SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS) {
            return defaultInfectiousness;
        }
        return daysSinceOnsetToInfectiousness.get(
                daysSinceOnsetOfSymptoms + MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET);
    }

    public static ReportType getReportType(
            @com.google.samples.exposurenotification.nearby.ReportType int reportTypeValue,
            ReportType defaultReportType) {
        ReportType reportType = ReportType.forNumber(reportTypeValue);
        if (reportType == null || REPORT_TYPE_UNKNOWN.equals(reportType)) {
            reportType = defaultReportType;
        }
        return reportType;
    }

    private ExposureWindowUtils() {
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import android.util.Pair;

import androidx.annotation.Nullable;

import com.google.common.collect.Iterables;
import com.google.samples.exposurenotification.nearby.ExposureConfiguration;
import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKey.ReportType;
import com.google.samples.exposurenotification.ExposureNotificationEnums.Infectiousness;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.features.ContactTracingFeature;
import com.google.samples.exposurenotification.storage.ExposureRecord;
import com.google.samples.exposurenotification.storage.ExposureResult;
import com.google.samples.exposurenotification.storage.ExposureWindowProto;
import com.google.samples.exposurenotification.storage.ScanInstanceProto;
import com.google.samples.exposurenotification.storage.SightingRecord;
import com.google.samples.exposurenotification.storage.TekMetadata;
import com.google.samples.exposurenotification.storage.TekMetadataRecord;

import org.joda.time.Duration;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * Handles the evaluation of exposure for a single diagnosis key and its sightings.
 *
 * <p>See {@link #findExposures(TemporaryExposureKey, List)}.
 */
final class KeyExposureEvaluator {
    private final ExposureConfiguration exposureConfiguration;
    private final RiskScoreCalculator riskScoreCalculator;
    private final TracingParams tracingParams;
    private final String instanceLogTag;
    private final TekMetadataRecord tekMetadataRecord;

    /**
     * @param exposureConfiguration exposure configuration used to run evaluations with.
     * @param riskScoreCalculator   risk score calculator to generate scores with
     * @param tracingParams         tracing parameters for filtering exposures based on sightings
     * @param instanceLogTag        log tag associated with matching job. See {@link ExposureMatchingTracer}.
     * @param tekMetadataRecord     tek data mapping for creating {@link ExposureWindowProto}.
     */
    KeyExposureEvaluator(
            ExposureConfiguration exposureConfiguration,
            RiskScoreCalculator riskScoreCalculator,
            TracingParams tracingParams,
            String instanceLogTag,
            TekMetadataRecord tekMetadataRecord) {
        this.exposureConfiguration = exposureConfiguration;
        this.riskScoreCalculator = riskScoreCalculator;
        this.tracingParams = tracingParams;
        this.instanceLogTag = instanceLogTag;
        this.tekMetadataRecord = tekMetadataRecord;
    }

    /**
     * Finds exposure windows given the time-sorted sightings seen from the provided {@link
     * TemporaryExposureKey}.
     *
     * <p>Exposures to one TEK are split in windows of 30 minutes, so a given TEK may lead to several
     * exposure windows if beacon sightings for it spanned more than 30 minutes.
     *
     * @param diagnosisKey    The diagnosis key whose rolling proximity identifiers were sighted in the
     *                        provided list of sightings.
     * @param sightingRecords a list of rolling proximity id sightings.
     * @return list of exposure windows for the diagnosisKey.
     */
    List<ExposureWindowProto> findExposureWindows(
            TemporaryExposureKey diagnosisKey, List<SightingRecordWithMetadata> sightingRecords) {
        List<ExposureWindowProto> exposureWindows = new ArrayList<>();

        ExposureWindowProto.Builder currentExposureWindowBuilder = null;
        int previousSightingEpochSeconds = 0;
        Infectiousness infectiousness =
                ExposureWindowUtils.getInfectiousness(
                        diagnosisKey.getDaysSinceOnsetOfSymptoms(),
                        tekMetadataRecord.getDaysSinceOnsetToInfectiousnessList(),
                        tekMetadataRecord.getInfectiousnessWhenDaysSinceOnsetMissing());
        ReportType reportType =
                ExposureWindowUtils.getReportType(
                        diagnosisKey.getReportType(), tekMetadataRecord.getReportTypeWhenMissing());
        for (SightingRecordWithMetadata sightingRecordWithMetadata : sightingRecords) {
            if (shouldStartNewExposureWindow(sightingRecordWithMetadata, currentExposureWindowBuilder)) {
                if (currentExposureWindowBuilder != null) {
                    exposureWindows.add(currentExposureWindowBuilder.build());
                }
                currentExposureWindowBuilder =
                        ExposureWindowProto.newBuilder()
                                .setEpochSeconds(sightingRecordWithMetadata.sightingRecord().getEpochSeconds())
                                .setTekMetadata(
                                        TekMetadata.newBuilder()
                                                .setInfectiousness(infectiousness)
                                                .setReportType(reportType)
                                                .setCalibrationConfidence(
                                                        sightingRecordWithMetadata.metadata().calibrationConfidence()));
            }
            if (sightingInTheSameScanInstance(sightingRecordWithMetadata, previousSightingEpochSeconds)
                    && currentExposureWindowBuilder.getScanInstancesCount() > 0) {
                // Add to the last existing scan instance.
                currentExposureWindowBuilder.setScanInstances(
                        currentExposureWindowBuilder.getScanInstancesCount() - 1,
                        currentExposureWindowBuilder
                                .getScanInstances(currentExposureWindowBuilder.getScanInstancesCount() - 1)
                                .toBuilder()
                                .addAttenuations(sightingRecordWithMetadata.attenuationValue()));
            } else {
                // Create a new ScanInstance.
                currentExposureWindowBuilder.addScanInstances(
                        ScanInstanceProto.newBuilder()
                                .addAttenuations(sightingRecordWithMetadata.attenuationValue())
                                .setSecondsSinceLastScan(
                                        sightingRecordWithMetadata.secondsSinceLastScan(
                                                (int) ContactTracingFeature.maxMinutesSinceLastScan(),
                                                (int) ContactTracingFeature.defaultMinutesSinceLastScan())));
            }
            previousSightingEpochSeconds = sightingRecordWithMetadata.sightingRecord().getEpochSeconds();
        }

        if (currentExposureWindowBuilder != null) {
            exposureWindows.add(currentExposureWindowBuilder.build());
        }
        return exposureWindows;
    }

    /**
     * Finds exposures given the time-sorted sightings seen from the provided {@link
     * TemporaryExposureKey}.
     *
     * <p>Exposures are defined as consecutive sightings where the time between each sighting is not
     * longer than {@link TracingParams#maxInterpolationDuration()} and the exposure duration is not
     * shorter than {@link TracingParams#minExposureBucketizedDuration()}.
     *
     * <p>Exposure durations are rounded into five minute intervals before being evaluated against
     * {@link TracingParams#minExposureBucketizedDuration()}.
     *
     * @param diagnosisKey    The diagnosis key whose rolling proximity identifiers were sighted in the
     *                        provided list of sightings.
     * @param sightingRecords a list of rolling proximity id sightings.
     * @return exposure result containing list of exposures.
     */
    @Nullable
    ExposureResult findExposures(
            TemporaryExposureKey diagnosisKey, List<SightingRecordWithMetadata> sightingRecords) {
        if (sightingRecords.isEmpty()) {
            Log.log.atInfo().log("%s No sighting records.", instanceLogTag);
            return null;
        }
        List<ExposureRecord> exposureRecords = new ArrayList<>();
        // The average is computed using time spent at
        // attenuation levels as weights. The attenuation level between two scans is
        // the attenuation level of the first scan. The first and last scans get extended by half of
        // scanInterval. Consider:
        //
        // scanInterval = 6
        // t=00, attenuation=1
        // t=10, attenuation=4
        // t=50, attenuation=5
        //
        // The resulting attenuation value is:
        //
        // (6/2 * 1 + (10 - 0) * 1 + (50 - 10) * 4 + 6/2 * 5) / (3 + 10 + 40 +3) = 188 / 56 = 3.36
        //
        // The result is rounded to 3.

        int exposureFirstSightingSeconds = sightingRecords.get(0).sightingRecord().getEpochSeconds();
        List<TimeAndAttenuation> timeAndAttenuations = new ArrayList<>();

        int maxRiskScore = 0;
        int totalRiskScore = 0;
        List<Integer> totalTimesBelowBetweenAndAbove = Arrays.asList(0, 0, 0);

        for (int i = 0; i < sightingRecords.size(); ++i) {
            SightingRecord currentScan = sightingRecords.get(i).sightingRecord();
            timeAndAttenuations.add(
                    TimeAndAttenuation.create(
                            currentScan.getEpochSeconds(), sightingRecords.get(i).attenuationValue()));

            if (isEndOfPossibleExposureSightings(
                    i, sightingRecords, tracingParams.maxInterpolationDuration())) {
                Log.log.atVerbose().log("%s Found a series of sightings.", instanceLogTag);
                // 5 minutes bucketization, as defined in the API.
                Duration bucketizedDuration =
                        bucketizeDuration(
                                Duration.standardSeconds(
                                        currentScan.getEpochSeconds() - exposureFirstSightingSeconds),
                                Duration.standardMinutes(5));

                List<Period> periods =
                        computePeriods(timeAndAttenuations, tracingParams.scanInterval().getStandardSeconds());
                int weightedAttenuation = weightAttenuationOfPeriods(periods);
                int[] durationAtAttenuationThresholds =
                        exposureConfiguration.getDurationAtAttenuationThresholds();
                List<Integer> timesBelowBetweenAndAbove =
                        getTimeBelowBetweenAndAbove(
                                periods,
                                durationAtAttenuationThresholds[0],
                                durationAtAttenuationThresholds[1],
                                tracingParams.interpolationEnabled());

                Log.log
                        .atInfo()
                        .log(
                                "%s Bucketed duration=%dm >= min_duration=%dm ? %b.",
                                instanceLogTag,
                                bucketizedDuration.getStandardMinutes(),
                                tracingParams.minExposureBucketizedDuration().getStandardMinutes(),
                                !bucketizedDuration.isShorterThan(tracingParams.minExposureBucketizedDuration()));
                if (!bucketizedDuration.isShorterThan(tracingParams.minExposureBucketizedDuration())) {
                    Log.log.atVerbose().log("%s Found exposure.", instanceLogTag);
                    ExposureRecord exposureRecord =
                            ExposureRecord.newBuilder()
                                    .setAttenuationValue(weightedAttenuation)
                                    .setDurationSeconds((int) bucketizedDuration.getStandardSeconds())
                                    .addAllAttenuationDurations(timesBelowBetweenAndAbove)
                                    .setTransmissionRiskLevel(diagnosisKey.getTransmissionRiskLevel())
                                    .build();

                    long dateMillisSinceEpoch =
                            Duration.standardMinutes(
                                    diagnosisKey.getRollingStartIntervalNumber()
                                            * ContactTracingFeature.idRollingPeriodMinutes())
                                    .getMillis();
                    int riskScore =
                            riskScoreCalculator.calculateRiskScore(
                                    diagnosisKey, exposureRecord, dateMillisSinceEpoch, exposureConfiguration);
                    maxRiskScore = Math.max(riskScore, maxRiskScore);
                    totalRiskScore += riskScore;
                    for (int bin = 0; bin < totalTimesBelowBetweenAndAbove.size(); bin++) {
                        totalTimesBelowBetweenAndAbove.set(
                                bin, totalTimesBelowBetweenAndAbove.get(bin) + timesBelowBetweenAndAbove.get(bin));
                    }

                    exposureRecords.add(exposureRecord.toBuilder().setRiskScore(riskScore).build());
                }
                if (i + 1 == sightingRecords.size()) {
                    break;
                }
                exposureFirstSightingSeconds =
                        sightingRecords.get(i + 1).sightingRecord().getEpochSeconds();
                timeAndAttenuations.clear();
            }
        }
        if (exposureRecords.isEmpty()) {
            return null;
        }
        ReportType reportType =
                ExposureWindowUtils.getReportType(
                        diagnosisKey.getReportType(), tekMetadataRecord.getReportTypeWhenMissing());
        return ExposureResult.newBuilder()
                .addAllExposureRecords(exposureRecords)
                .setMaxRiskScore(maxRiskScore)
                .setDateMillisSinceEpoch(
                        Duration.standardMinutes(
                                diagnosisKey.getRollingStartIntervalNumber()
                                        * ContactTracingFeature.idRollingPeriodMinutes())
                                .getMillis())
                .setTracingParamsRecord(tracingParams.toTracingParamsRecord())
                .setTotalRiskScore(totalRiskScore)
                .addAllAttenuationDurations(totalTimesBelowBetweenAndAbove)
                .setTekMetadata(TekMetadata.newBuilder().setReportType(reportType))
                .setReportTypeTransitionCount(0)
                .build();
    }

    private static boolean shouldStartNewExposureWindow(
            SightingRecordWithMetadata sightingRecordWithMetadata,
            ExposureWindowProto.Builder currentExposureWindowBuilder) {
        return currentExposureWindowBuilder == null
                || sightingRecordWithMetadata.sightingRecord().getEpochSeconds()
                - currentExposureWindowBuilder.getEpochSeconds()
                >= MINUTES.toSeconds(ContactTracingFeature.maxExposureWindowDurationMinutes());
    }

    private static boolean sightingInTheSameScanInstance(
            SightingRecordWithMetadata sightingRecordWithMetadata, int previousSightingEpochSeconds) {
        // Sightings are considered to be in the same scan if they are separate by a relatively short
        // duration. The exact value here is not important, as scanning durations are at the order of
        // seconds vs. time between scans are at the order of minutes. We use 1.5 * max scan duration as
        // a simple heuristic.
        return sightingRecordWithMetadata.sightingRecord().getEpochSeconds()
                - previousSightingEpochSeconds
                <= 1.5
                * (ContactTracingFeature.scanTimeSeconds()
                + ContactTracingFeature.scanTimeExtendForProfileInUseSeconds());
    }

    /**
     * Returns a list of {@link Period}s with duration > 0.
     */
    private static List<Period> computePeriods(
            List<TimeAndAttenuation> timeAndAttenuations, long scanInterval) {
        // Add fake start and end boundary scans with the same attenuation as first and last ones.
        int timeMargin = (int) (scanInterval / 2);
        timeAndAttenuations.add(
                0,
                TimeAndAttenuation.create(
                        timeAndAttenuations.get(0).time() - timeMargin,
                        timeAndAttenuations.get(0).attenuation()));
        timeAndAttenuations.add(
                TimeAndAttenuation.create(
                        Iterables.getLast(timeAndAttenuations).time() + timeMargin,
                        Iterables.getLast(timeAndAttenuations).attenuation()));

        List<Period> periods = new ArrayList<>();

        TimeAndAttenuation previous = timeAndAttenuations.get(0);
        for (TimeAndAttenuation timeAndAttenuation : timeAndAttenuations) {
            // Nothing to do for 0-length segments.
            if (timeAndAttenuation.time().equals(previous.time())) {
                continue;
            }
            periods.add(Period.create(previous, timeAndAttenuation));
            previous = timeAndAttenuation;
        }

        return periods;
    }

    /**
     * Calculates a time-weighted attenuation value.
     */
    private static int weightAttenuationOfPeriods(List<Period> periods) {
        double attenuationSum = 0;
        double durationSum = 0;
        for (Period period : periods) {
            attenuationSum += period.scan1().attenuation() * period.duration();
            durationSum += period.duration();
        }
        return (int) Math.round(attenuationSum / durationSum);
    }

    private static List<Integer> getTimeBelowBetweenAndAbove(
            List<Period> periods, int thresholdLow, int thresholdHigh, boolean interpolate) {
        Pair<Integer, Integer> timeBelowAndAboveOfLow =
                getTimeBelowAndAbove(periods, thresholdLow, interpolate);
        Pair<Integer, Integer> timeBelowAndAboveOfHigh =
                getTimeBelowAndAbove(periods, thresholdHigh, interpolate);

        int timeBelowLow = timeBelowAndAboveOfLow.first;
        int timeAboveHigh = timeBelowAndAboveOfHigh.second;
        int totalTime = timeBelowAndAboveOfLow.first + timeBelowAndAboveOfLow.second;
        // Written explicitly to avoid confusion.
        int timeBetween = totalTime - timeBelowLow - timeAboveHigh;

        return Arrays.asList(timeBelowLow, timeBetween, timeAboveHigh);
    }

    /**
     * Calculates duration below and above the given threshold.
     *
     * <p>Set {@code interpolate} to {@code true} to use linear interpolation of the attenuation
     * values.
     */
    private static Pair<Integer, Integer> getTimeBelowAndAbove(
            List<Period> periods, int threshold, boolean interpolate) {
        int timeAboveThreshold = 0;
        int timeBelowThreshold = 0;
        for (Period period : periods) {

            int timeCross = period.calculateTimeCross(threshold, interpolate);

            // No cross in the interval.
            if (timeCross <= period.scan1().time() || timeCross >= period.scan2().time()) {
                if (period.scan1().attenuation() >= threshold) {
                    // The entire interval is above the threshold.
                    timeAboveThreshold += period.duration();
                } else {
                    // The entire interval is below threshold.
                    timeBelowThreshold += period.duration();
                }
            } else {
                // Cross in the interval, see how much is above and below.
                int leftTime = timeCross - period.scan1().time();
                int rightTime = period.scan2().time() - timeCross;
                if (period.scan1().attenuation() >= threshold) {
                    // The left hand side is above threshold
                    timeAboveThreshold += leftTime;
                    timeBelowThreshold += rightTime;
                } else {
                    // The right hand side is above the threshold
                    timeAboveThreshold += rightTime;
                    timeBelowThreshold += leftTime;
                }
            }
        }
        return new Pair<>(timeBelowThreshold, timeAboveThreshold);
    }

    /**
     * Returns true if the next {@link SightingRecord} is too far away in time from current {@link
     * SightingRecord}.
     *
     * @param index                    current index in question for end of sightings series or not.
     * @param sightingRecords          scan records in ascending order of scan time.
     * @param maxInterpolationDuration duration threshold for two {@link SightingRecord}s being too
     *                                 far away to be a part of the same exposure.
     */
    private static boolean isEndOfPossibleExposureSightings(
            int index,
            List<SightingRecordWithMetadata> sightingRecords,
            Duration maxInterpolationDuration) {
        // Last item always marks end of exposure sightings.
        if (index + 1 == sightingRecords.size()) {
            return true;
        }
        return Duration.standardSeconds(
                sightingRecords.get(index + 1).sightingRecord().getEpochSeconds()
                        - sightingRecords.get(index).sightingRecord().getEpochSeconds())
                .isLongerThan(maxInterpolationDuration);
    }

    /**
     * For every observation of a sighting (which occurs at {@code scanInterval}, our best estimate
     * for the duration of exposure is {@code scanInterval}. Therefore, to calculate exposure
     * duration, {@code scanInterval} must be added and the estimate must be rounded to the nearest
     * {@code scanInterval} increment.
     */
    private static Duration bucketizeDuration(Duration sightedScanDuration, Duration scanInterval) {
        Duration estimatedExposureDuration = sightedScanDuration.plus(scanInterval);
        long mod = estimatedExposureDuration.getStandardSeconds() % scanInterval.getStandardSeconds();
        if (mod <= scanInterval.getStandardSeconds() / 2) {
            return estimatedExposureDuration.minus(Duration.standardSeconds(mod));
        }
        return estimatedExposureDuration.plus(scanInterval.minus(Duration.standardSeconds(mod)));
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import android.content.Context;
import android.util.Pair;
import com.google.common.collect.ImmutableSet;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.samples.exposurenotification.ExposureKeyExportProto;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.data.fileformat.TemporaryExposureKeyConverter;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/** Implements generate id and key matching under native code. */
public class MatchingJni implements AutoCloseable {

    private static native long initNative(byte[][] bleScanResults);

    /**
     * Returns the {@link ExposureKeyExportProto.TemporaryExposureKey} array. Each element is the a
     * serialized byte array and can be converted by {@link
     * ExposureKeyExportProto.TemporaryExposureKey#parseFrom(byte[])}
     */
    private static native byte[][] matchingNative(long nativePtr, String[] keyFiles);

    private static native int[] matchingLegacyNative(
            long nativePtr, byte[][] tempKeys, int[] rollingStartIntervalNumber, int currentKeyIndex);

    /**
     * Returns the processed key count which are filtered by invoking {@link #matchingNative}. If the
     * {@code nativePtr} is invalid, returns -1.
     */
    private static native int lastProcessedKeyCountNative(long nativePtr);

    private static native void releaseNative(long nativePtr);

    private final long nativePtr;

    public static boolean loadNativeLibrary(Context context) {
        System.loadLibrary("matching");
        return true;
    }

    public MatchingJni(Context context, byte[][] bleScanResults) {
        loadNativeLibrary(context);
        this.nativePtr = initNative(bleScanResults);
        Log.log.atInfo().log("MatchingJni get native ptr %d", nativePtr);
    }

    public ImmutableSet<TemporaryExposureKey> matching(List<String> keyFiles) {
        byte[][] protoArray = matchingNative(nativePtr, keyFiles.toArray(new String[0]));
        if (protoArray == null) {
            Log.log.atInfo().log("MatchingJni get nullable key set from native.");
            return ImmutableSet.of();
        }

        ImmutableSet.Builder<TemporaryExposureKey> keySet = new ImmutableSet.Builder<>();
        for (byte[] proto : protoArray) {
            try {
                ExposureKeyExportProto.TemporaryExposureKey key =
                        ExposureKeyExportProto.TemporaryExposureKey.parseFrom(proto);
                TemporaryExposureKey convertedKey = new TemporaryExposureKeyConverter().convert(key);
                if (convertedKey == null) {
                    Log.log.atWarning().log("MatchingJni failed to convert the proto key.");
                    continue;
                }
                keySet.add(convertedKey);
            } catch (InvalidProtocolBufferException e) {
                Log.log.atWarning().withCause(e).log("MatchingJni failed to parse the proto byte.");
            }
        }
        return keySet.build();
    }

    public Pair<Integer, Set<TemporaryExposureKey>> matchingLegacy(
            Iterable<TemporaryExposureKey> temporaryExposureKeys) {
        int matchingWithNativeBufferKeySize =
                (int) ContactTracingFeature.matchingWithNativeBufferKeySize();
        Set<TemporaryExposureKey> result = new HashSet<>();
        TemporaryExposureKey[] keys = new TemporaryExposureKey[matchingWithNativeBufferKeySize];
        int keyCount = 0;
        byte[][] keyBuffer = new byte[matchingWithNativeBufferKeySize][];
        int[] rollingStartIntervalNumberBuffer = new int[matchingWithNativeBufferKeySize];
        int currentKeyIndex = 0;
        for (TemporaryExposureKey diagnosisKey : temporaryExposureKeys) {
            keyCount++;
            keyBuffer[currentKeyIndex] = diagnosisKey.getKeyData();
            rollingStartIntervalNumberBuffer[currentKeyIndex] =
                    diagnosisKey.getRollingStartIntervalNumber();
            keys[currentKeyIndex] = diagnosisKey;
            currentKeyIndex++;
            if (currentKeyIndex >= matchingWithNativeBufferKeySize) {
                processingResult(
                        matchingLegacyNative(
                                nativePtr, keyBuffer, rollingStartIntervalNumberBuffer, currentKeyIndex),
                        keys,
                        result);
                currentKeyIndex = 0;
            }
        }

        if (currentKeyIndex > 0) {
            processingResult(
                    matchingLegacyNative(
                            nativePtr, keyBuffer, rollingStartIntervalNumberBuffer, currentKeyIndex),
                    keys,
                    result);
        }
        Log.log
                .atInfo()
                .log(
                        "ExposureMatchingTracer.traceWithNative() %d keys matched, total %d keys",
                        result.size(), keyCount);
        return Pair.create(keyCount, result);
    }

    private static void processingResult(
            int[] matchedIdIndexes, TemporaryExposureKey[] keys, Set<TemporaryExposureKey> result) {
        if (matchedIdIndexes == null || matchedIdIndexes.length <= 0) {
            return;
        }
        for (int matchedIdIndex : matchedIdIndexes) {
            result.add(keys[matchedIdIndex]);
        }
    }

    public int getLastProcessedKeyCount() {
        return lastProcessedKeyCountNative(nativePtr);
    }

    @Override
    public void close() {
        releaseNative(nativePtr);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import androidx.annotation.Nullable;

import com.google.common.collect.ImmutableList;
import com.google.samples.exposurenotification.nearby.ExposureConfiguration;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.data.fileformat.StreamingTemporaryExposureKeyFileParser;
import com.google.samples.exposurenotification.storage.CloseableIterable;
import com.google.samples.exposurenotification.storage.CloseableIterables;
import com.google.samples.exposurenotification.storage.ExposureConfigurationConverter;
import com.google.samples.exposurenotification.storage.ExposureConfigurationProto;
import com.google.samples.exposurenotification.storage.MatchingRequestProto;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static com.google.samples.exposurenotification.Log.log;

/**
 * Encloses information about a matching request.
 */
public class MatchingRequest {
    private final long requestId;
    private final String shortenedPackageName;
    private final MatchingRequestProto matchingRequestProto;

    public MatchingRequest(long requestId, MatchingRequestProto matchingRequestProto) {
        this.requestId = requestId;
        this.matchingRequestProto = matchingRequestProto;
        shortenedPackageName = createShortenedPackageName(matchingRequestProto.getPackageName());
    }

    /**
     * The generated requestId.
     */
    public long requestId() {
        return requestId;
    }

    /**
     * Package name of client request.
     */
    public String packageName() {
        return matchingRequestProto.getPackageName();
    }

    /**
     * Gets the shortened version of the package name, used primarily for logging.
     */
    public String getShortenedPackageName() {
        return shortenedPackageName;
    }

    /**
     * Signature hash of client app.
     */
    public byte[] signatureHash() {
        return matchingRequestProto.getSignatureHash().toByteArray();
    }

    /**
     * Token associated with request.
     */
    public String token() {
        return matchingRequestProto.getToken();
    }

    /**
     * The SHA-256 has associated with the key files in this matching request, in base64.
     */
    public String keyFilesHash() {
        return matchingRequestProto.getKeyFilesHash();
    }

    public long requestTimeMillis() {
        return matchingRequestProto.getRequestTimeMillis();
    }

    /**
     * exposure configuration for request.
     */
    public ExposureConfiguration exposureConfiguration() {
        return new ExposureConfigurationConverter()
                .reverse()
                .convert(matchingRequestProto.getExposureConfiguration());
    }

    public ExposureConfigurationProto exposureConfigurationProto() {
        return matchingRequestProto.getExposureConfiguration();
    }

    /**
     * Gets the diagnosis keys provided with this request. The returned closable must be closed
     * after use so that the underlying resources (files) can be closed correctly.
     */
    public CloseableIterable<TemporaryExposureKey> diagnosisKeys() {
        List<CloseableIterable<TemporaryExposureKey>> keyIterables = new ArrayList<>();
        for (String filePath : matchingRequestProto.getKeyFilesList()) {
            try {
                keyIterables.add(StreamingTemporaryExposureKeyFileParser.parse(new File(filePath)));
            } catch (IOException e) {
                log.atSevere().withCause(e).log("Can't parse keys from %s", filePath);
            }
        }
        return CloseableIterables.concat(keyIterables);
    }

    public ImmutableList<String> diagnosisKeyFiles() {
        return ImmutableList.copyOf(matchingRequestProto.getKeyFilesList());
    }

    @Override
    public int hashCode() {
        return Objects.hash(requestId, matchingRequestProto);
    }

    @Override
    public boolean equals(@Nullable Object object) {
        if (object instanceof MatchingRequest) {
            MatchingRequest that = (MatchingRequest) object;
            return this.requestId == that.requestId
                    && this.matchingRequestProto.equals(that.matchingRequestProto);
        }
        return false;
    }

    /**
     * Shortens package name to the string past the second to last period.
     */
    static String createShortenedPackageName(String packageName) {
        if (packageName.lastIndexOf(".") == -1) {
            return packageName;
        }
        return packageName.substring(
                packageName.lastIndexOf(".", packageName.lastIndexOf(".") - 1) + 1);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import com.google.auto.value.AutoValue;

/** A pair of two scans. */
@AutoValue
abstract class Period {
    static Period create(TimeAndAttenuation scan1, TimeAndAttenuation scan2) {
        return new AutoValue_Period(scan1, scan2);
    }

    abstract TimeAndAttenuation scan1();

    abstract TimeAndAttenuation scan2();

    public int duration() {
        return scan2().time() - scan1().time();
    }

    public int attenuationDiff() {
        return scan2().attenuation() - scan1().attenuation();
    }

    public int calculateTimeCross(int threshold, boolean interpolate) {
        int timeCross = scan1().time();
        if (interpolate && attenuationDiff() != 0) {
            // calculate the time at which the interpolated attenuation equals the threshold.
            timeCross =
                    (int)
                            Math.round(
                                    scan1().time()
                                            + (threshold - scan1().attenuation())
                                            / (double) attenuationDiff()
                                            * duration());
        }
        return timeCross;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import android.content.Context;

import com.google.auto.value.AutoValue;
import com.google.common.collect.Lists;
import com.google.samples.exposurenotification.ExposureKeyExportProto.TEKSignatureList;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipInputStream;

/**
 * This class is a heavily modified version of the original code which accepts a list of signed
 * diagnosis key ZIP files that match the file format described at
 * https://developers.google.com/android/exposure-notifications/exposure-key-file-format.
 * <p>
 * This code demonstrates how to call the required functions to verify the file format is
 * correct and the keys have been properly signed.
 * <p>
 * The code has been greatly simplified from the original version, primarily in the way errors
 * are handled.
 */
public class ProvideDiagnosisKeys {
    private final Context context;

    public ProvideDiagnosisKeys(Context context) {
        this.context = context;
    }

    public boolean verify(String callingPackage, File signedKeyFile) {
        KeyFileSignature keyFileSignature = unzip(context, signedKeyFile);
        ArrayList<KeyFileSignature> signatures = Lists.newArrayList(keyFileSignature);

        SignatureVerifier signatureVerifier = new SignatureVerifier();
        try {
            return signatureVerifier.verify(callingPackage, signatures);
        } catch (Exception e) {
            throw new RuntimeException("Verification failed", e);
        }
    }

    /**
     * Unzips a diagnosis keys zip file into an internal folder to allow for
     * verification of the signature and processing of the keys by {@link MatchingJni}
     * <p>
     * FIXME: This code is NOT threadsafe and is incomplete as it exists. Key files are extracted
     * to a folder and those keys + signatures are not cleaned up here (or anywhere else in the
     * provided snippets). After matching occurs, these files should be removed by whatever
     * system is running the key matching process.
     *
     * @param context           The context to use
     * @param diagnosisKeysFile The zip file
     * @return A {@link KeyFileSignature} that can be verified by {@link SignatureVerifier}
     */
    static KeyFileSignature unzip(Context context, File diagnosisKeysFile) {
        File targetFolder = new File(context.getFilesDir(), "en_diagnosis_keys");
        if (!targetFolder.mkdirs()) {
            throw new RuntimeException("Can't create folder for keys");
        }

        ArrayList<KeyFileSignature> results = new ArrayList<>();
        byte[] buffer = new byte[2048];
        TEKSignatureList signatureList = null;
        File keyFile = null;

        try {
            // We use a ZipInputStream, rather than a ZipFile, to minimize the memory footprint.
            ZipInputStream zipStream = new ZipInputStream(new FileInputStream(diagnosisKeysFile));

            ZipEntry entry;
            while ((entry = zipStream.getNextEntry()) != null) {
                String entryName = getValidatedName(entry);
                if (entryName.equals(ContactTracingFeature.diagnosisKeySignatureFileName())) {
                    if (signatureList != null) {
                        deleteFile(targetFolder);
                        throw new IllegalStateException("Archive contains multiple " + entryName);
                    }
                    signatureList =
                            TEKSignatureList.parseFrom(zipStream);
                } else if (entryName.equals(ContactTracingFeature.diagnosisKeyBinFileName())) {
                    if (keyFile != null) {
                        deleteFile(targetFolder);
                        throw new IllegalStateException("Archive contains multiple " + entryName);
                    }
                    keyFile = new File(targetFolder, "export.bin");

                    if (!keyFile.createNewFile()) {
                        deleteFile(targetFolder);
                        throw new RuntimeException(
                                "Can't create file " + keyFile.getAbsolutePath());
                    }
                    try (FileOutputStream output = new FileOutputStream(keyFile.getAbsolutePath())) {
                        int len;
                        while ((len = zipStream.read(buffer)) > 0) {
                            output.write(buffer, 0, len);
                        }
                    }
                } else {
                    deleteFile(targetFolder);
                    throw new IllegalStateException("Invalid key file entry " + entryName);
                }
            }
        } catch (IOException e) {
            deleteFile(targetFolder);
            throw new RuntimeException("Failed to extract diagnosis keys from " +
                    diagnosisKeysFile.getAbsolutePath(), e);
        }

        if (keyFile == null || signatureList == null) {
            deleteFile(targetFolder);
            throw new RuntimeException("Invalid file content: " +
                    diagnosisKeysFile.getAbsolutePath());
        }
        return KeyFileSignature.create(keyFile, signatureList);
    }

    /**
     * Return the name of a ZipEntry after attempting to verifying that it does not exploit any
     * path traversal attacks.
     *
     * @throws ZipException if {@param zipEntry} contains any possible path traversal
     *                      characters.
     */
    private static String getValidatedName(ZipEntry zipEntry) throws ZipException {
        String name = zipEntry.getName();
        if (name.contains("..")) {
            // If the string does contain "..", break it down into its actual name
            // elements to ensure it actually contains ".." as a name, not just a
            // name like "foo..bar" or even "foo..", which should be fine.
            File file = new File(name);
            while (file != null) {
                if (file.getName().equals("..")) {
                    throw new ZipException("Illegal name: " + name);
                }
                file = file.getParentFile();
            }
        }
        return name;
    }

    private static boolean deleteFile(File file) {
        if (file.isDirectory()) {
            File[] children = file.listFiles();
            if (children != null) {
                for (File child : children) {
                    deleteFile(child);
                }
            }
        }

        return file.delete();
    }

    @AutoValue
    abstract static class KeyFileSignature implements SignatureVerifier.KeyExport {
        static KeyFileSignature create(File keyFile, TEKSignatureList signatureList) {
            return new AutoValue_ProvideDiagnosisKeys_KeyFileSignature(keyFile, signatureList);
        }

        /**
         * The file of the diagnosis keys.
         */
        abstract File keyFile();

        /**
         * The signature list that was read from export.sig file.
         */
        @Override
        public abstract TEKSignatureList signatureList();

        @Override
        public InputStream openKeyInputStream() throws IOException {
            return new FileInputStream(keyFile());
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import com.google.common.annotations.VisibleForTesting;
import com.google.samples.Clock;
import com.google.samples.Clock.DefaultClock;
import com.google.samples.exposurenotification.nearby.ExposureConfiguration;
import com.google.samples.exposurenotification.nearby.RiskLevel;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.features.ContactTracingFeature;
import com.google.samples.exposurenotification.storage.ExposureRecord;

import java.util.List;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Used to calculate risk score based on client configuration and exposures.
 */
public class RiskScoreCalculator {
    private final Clock clock;

    public static RiskScoreCalculator create() {
        return new RiskScoreCalculator(DefaultClock.getInstance());
    }

    public static RiskScoreCalculator create(Clock clock) {
        return new RiskScoreCalculator(clock);
    }

    @VisibleForTesting
    RiskScoreCalculator(Clock clock) {
        this.clock = clock;
    }

    /**
     * Returns a V1 risk score. Returns 0 if calculated risk score is below {@link
     * ExposureConfiguration#getMinimumRiskScore()}. Throws IllegalArgumentException on invalid input.
     */
    public int calculateRiskScore(
            TemporaryExposureKey diagnosisKey,
            ExposureRecord exposureRecord,
            long dateMillisSinceEpoch,
            ExposureConfiguration configuration) {

        int attenuationScore =
                configuration
                        .getAttenuationScores()[bucketAttenuationValue(exposureRecord.getAttenuationValue())];
        int daysSinceLastExposureScore =
                configuration.getDaysSinceLastExposureScores()[bucketLatencyDays(dateMillisSinceEpoch)];
        int durationScore =
                configuration.getDurationScores()[bucketDuration(exposureRecord.getDurationSeconds())];
        int transmissionRiskScore =
                diagnosisKey.getTransmissionRiskLevel() == RiskLevel.RISK_LEVEL_INVALID
                        ? 1
                        : configuration.getTransmissionRiskScores()[bucketRiskLevel(diagnosisKey)];
        int riskScore =
                attenuationScore * daysSinceLastExposureScore * durationScore * transmissionRiskScore;

        if (riskScore >= configuration.getMinimumRiskScore()) {
            return riskScore;
        } else {
            return 0;
        }
    }

    public static int getAttenuationScore(
            int weightedAttenuation, ExposureConfiguration configuration) {
        return configuration.getAttenuationScores()[bucketAttenuationValue(weightedAttenuation)];
    }

    private int bucketRiskLevel(TemporaryExposureKey diagnosisKey) {
        // Enum of risk levels starts at 1, with 0 reserved for RISK_LEVEL_INVALID.
        return diagnosisKey.getTransmissionRiskLevel() - 1;
    }

    private static int bucketAttenuationValue(int attenuationValue) {
        List<Integer> bucketBounds =
                ContactTracingFeature.riskScoreAttenuationValueBuckets();
        for (int bucket = 0; bucket < bucketBounds.size(); bucket++) {
            if (attenuationValue > bucketBounds.get(bucket)) {
                return bucket;
            }
        }
        return bucketBounds.size();
    }

    private int bucketLatencyDays(long dateMillisSinceEpoch) {
        List<Integer> bucketBounds =
                ContactTracingFeature.riskScoreLatencyDaysBuckets();
        long timeNow = clock.currentTimeMillis();
        long latencyDays = MILLISECONDS.toDays(timeNow - dateMillisSinceEpoch);
        for (int bucket = 0; bucket < bucketBounds.size(); bucket++) {
            if (latencyDays >= bucketBounds.get(bucket)) {
                return bucket;
            }
        }
        return bucketBounds.size();
    }

    private int bucketDuration(long durationSeconds) {
        List<Integer> bucketBounds = ContactTracingFeature.riskScoreDurationBuckets();
        long durationMinutes = SECONDS.toMinutes(durationSeconds);
        for (int bucket = 0; bucket < bucketBounds.size(); bucket++) {
            if (durationMinutes <= bucketBounds.get(bucket)) {
                return bucket;
            }
        }
        return bucketBounds.size();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;

import com.google.auto.value.AutoValue;
import com.google.samples.exposurenotification.data.BluetoothMetadata;
import com.google.samples.exposurenotification.storage.SightingRecord;

/** A pair of {@link SightingRecord} and {@link BluetoothMetadata}. */
@AutoValue
abstract class SightingRecordWithMetadata {
    static SightingRecordWithMetadata create(
            SightingRecord sightingRecord, BluetoothMetadata metadata) {
        return new AutoValue_SightingRecordWithMetadata(sightingRecord, metadata);
    }

    abstract SightingRecord sightingRecord();

    /**
     * A decrypted version of {@link SightingRecord#getAssociatedEncryptedMetadata()} of {@link
     * #sightingRecord()}.
     */
    abstract BluetoothMetadata metadata();

    public int attenuationValue() {
        return Math.max(metadata().txPower() - sightingRecord().getRssi(), 0);
    }

    public int secondsSinceLastScan(int maxMinutesSinceLastScan, int defaultMinutesSinceLastScan) {
        // Default value, indicating we do not know when was the previous scan.
        if (sightingRecord().getPreviousScanEpochSeconds() == 0) {
            return (int) MINUTES.toSeconds(defaultMinutesSinceLastScan);
        }

        return (int)
                MINUTES.toSeconds(
                        Math.min(
                                maxMinutesSinceLastScan,
                                SECONDS.toMinutes(sightingRecord().getEpochSeconds())
                                        - SECONDS.toMinutes(sightingRecord().getPreviousScanEpochSeconds())));
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import android.content.Context;
import android.text.TextUtils;
import android.util.Base64;

import androidx.annotation.Nullable;

import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Splitter.MapSplitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.samples.exposurenotification.ExposureKeyExportProto.SignatureInfo;
import com.google.samples.exposurenotification.ExposureKeyExportProto.TEKSignature;
import com.google.samples.exposurenotification.ExposureKeyExportProto.TEKSignatureList;
import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKeyExport;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.features.ContactTracingFeature;
import com.google.samples.exposurenotification.matching.ProvideDiagnosisKeys.KeyFileSignature;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * Verified signatures of diagnosis key files, given a raw input stream to the file, and a metadata
 * proto object.
 */
public class SignatureVerifier {

    /**
     * The key export format for verification purpose.
     */
    public interface KeyExport {

        /**
         * The signature list that was read from export.sig file.
         */
        TEKSignatureList signatureList();

        /**
         * Opens input stream for diagnosis key file. Caller of this method is responsible for closing
         * it.
         */
        InputStream openKeyInputStream() throws IOException;
    }

    public static final String KEY_VERSION_JOINER = "-";

    // map of supported signature algorithms - add as necessary
    private static final ImmutableMap<String, String> OID_TO_NAME_MAP =
            ImmutableMap.of(
                    "1.2.840.10045.4.3.2", "SHA256withECDSA",
                    "1.2.840.10045.4.3.4", "SHA512withECDSA");

    private static final int STREAMING_BYTE_BUFFER_SIZE = 4096;

    @Nullable
    private byte[] keyFilesHash;
    @Nullable
    private String debugPublicKey;

    /**
     * Performs verification that the key exports (really {@link KeyFileSignature})s provided
     * have been properly signed by the PHA key that matches the app's package. It also
     * performs matching for co-signatures in order to support keys signed by multiple PHAs.
     *
     * @param callingPackage The calling package -- that is, the package name of the health
     *                       authority app calling this code.
     * @param exports        List of KeyFileSignature extracted by
     *                       {@link ProvideDiagnosisKeys#unzip(Context, File)}
     * @return {@code true} if the files were properly signed, {@code false} otherwise
     */
    public boolean verify(String callingPackage, List<? extends KeyExport> exports)
            throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException,
            SignatureException, ExecutionException, InterruptedException {
        Preconditions.checkArgument(!exports.isEmpty());

        // all chunks in batch must be present to verify
        BitSet batchChunks = new BitSet(exports.size());

        // b/157995134 would enable multiple whole batches and thus would need a map to verify.
        Map<TemporaryExposureKeyExport, BitSet> batchChunkMap = new HashMap<>();

        // all timestamps in a batch should match
        long startTime = Long.MIN_VALUE;
        long endTime = Long.MIN_VALUE;

        // retrieve public keys - same keys will be used throughout
        PartnerPublicKeySets partnerKeySet = getKeysForPackage(callingPackage, debugPublicKey);
        if (partnerKeySet.cosignSets.isEmpty()) {
            throw new SignatureException("no public key found for package " + callingPackage);
        }

        ExecutorService executor = Executors.newSingleThreadExecutor();
        MessageDigest keyHash = MessageDigest.getInstance("SHA-256");

        for (KeyExport export : exports) {
            TEKSignatureList tekSignatures = export.signatureList();
            if (tekSignatures.getSignaturesCount() == 0) {
                throw new SignatureException("data is unsigned");
            }

            // create list of verifications for each rotation. only one verification within the rotation
            // has to verify successfully. within each cosigned set, all keys must verify successfully for
            // the set to verify.
            List<Verification> setVerifications = new ArrayList<>(partnerKeySet.cosignSets.size());
            for (CosignSet cosignSet : partnerKeySet.cosignSets) {
                setVerifications.add(createCosignatureVerifications(tekSignatures, cosignSet));
            }

            // read bytes, and simultaneously update all signatures and divert them to another thread
            // where we will be reconstructing the metadata from those bytes
            TemporaryExposureKeyExport keyMetadata;
            try (PipedInputStream in = new PipedInputStream(STREAMING_BYTE_BUFFER_SIZE)) {
                Future<TemporaryExposureKeyExport> protoDecodeFuture;
                try (PipedOutputStream out = new PipedOutputStream(in);
                     InputStream is = export.openKeyInputStream()) {
                    protoDecodeFuture =
                            executor.submit(() -> TemporaryExposureKeyFileMetadataParser.parse(in));

                    // read stream and divert to secondary pipe and every signature
                    byte[] buffer = new byte[STREAMING_BYTE_BUFFER_SIZE];
                    int len = is.read(buffer);
                    while (len != -1) {
                        out.write(buffer, 0, len);
                        if (ContactTracingFeature.calculateDiagnosisKeyHash()) {
                            keyHash.update(buffer, 0, len);
                        }
                        for (Verification verification : setVerifications) {
                            verification.update(buffer, 0, len);
                        }
                        len = is.read(buffer);
                    }
                }

                // finalize signature verification - only one set needs to match
                boolean matched = false;
                for (Verification verification : setVerifications) {
                    if (verification.verify()) {
                        matched = true;
                        break;
                    }
                }
                if (!matched) {
                    Log.log.atWarning().log("The necessary signatures did not verify");
                    return false;
                }

                // retrieve metadata generated on other thread, and only now can we close the input stream
                keyMetadata = protoDecodeFuture.get();
            }

            if (keyMetadata.getBatchSize() > ContactTracingFeature.signatureMaxAllowedBatchGroupSize()) {
                throw new UnsupportedOperationException(
                        "Signature batch size is "
                                + keyMetadata.getBatchSize()
                                + ". It must not be greater than "
                                + ContactTracingFeature.signatureMaxAllowedBatchGroupSize()
                                + " for "
                                + createExposureKeyString(keyMetadata));
            }

            // verify start/end times...
            // When enableMultipleWholeBatchSignatureVerification is enabled, this is always verified as
            // start and end timestamps are present in the key.
            if (!ContactTracingFeature.enableMultipleWholeBatchSignatureVerification()) {
                if (startTime == Long.MIN_VALUE && endTime == Long.MIN_VALUE) {
                    startTime = keyMetadata.getStartTimestamp();
                    endTime = keyMetadata.getEndTimestamp();
                } else {
                    if (keyMetadata.getStartTimestamp() != startTime
                            || keyMetadata.getEndTimestamp() != endTime) {
                        throw new IOException(
                                "exposure key start/end times ("
                                        + keyMetadata.getStartTimestamp()
                                        + "/"
                                        + keyMetadata.getEndTimestamp()
                                        + ") do not match ("
                                        + startTime
                                        + "/"
                                        + endTime
                                        + ") for "
                                        + createExposureKeyString(keyMetadata));
                    }
                }
            }

            // verify metadata signature count...
            if (tekSignatures.getSignaturesCount() != keyMetadata.getSignatureInfosCount()) {
                throw new SignatureException(
                        "expected "
                                + keyMetadata.getSignatureInfosCount()
                                + " signatures, but found "
                                + tekSignatures.getSignaturesCount()
                                + " for "
                                + createExposureKeyString(keyMetadata));
            }

            // key for collecting all files belonging to the same batch
            TemporaryExposureKeyExport keyMetadataForMap =
                    keyMetadata.toBuilder().clearBatchNum().build();

            // verify batches, if fields are set
            if (keyMetadata.hasBatchSize()) {
                if (!keyMetadata.hasBatchNum()) {
                    throw new SignatureException(
                            "key metadata specifies batch size but not batch number for "
                                    + createExposureKeyString(keyMetadata));
                }

                // batch number is ordinal 1 based
                if (keyMetadata.getBatchNum() < 1) {
                    throw new SignatureException(
                            "batch number should be one or greater for " + createExposureKeyString(keyMetadata));
                }

                if (keyMetadata.getBatchNum() > keyMetadata.getBatchSize()) {
                    throw new SignatureException(
                            "batch number should not be greater than batch size for "
                                    + createExposureKeyString(keyMetadata));
                }
                if (ContactTracingFeature.enableMultipleWholeBatchSignatureVerification()) {
                    if (!batchChunkMap.containsKey(keyMetadataForMap)) {
                        batchChunkMap.put(keyMetadataForMap, new BitSet(keyMetadata.getBatchSize()));
                    }
                    // batch number is ordinal 1 based
                    batchChunkMap.get(keyMetadataForMap).set(keyMetadata.getBatchNum() - 1);

                } else {
                    if (keyMetadata.getBatchSize() != exports.size()) {
                        throw new SignatureException(
                                "signature batch size ("
                                        + keyMetadata.getBatchSize()
                                        + " does not match actual batch size ("
                                        + exports.size()
                                        + ") for "
                                        + createExposureKeyString(keyMetadata));
                    }
                    batchChunks.set(keyMetadata.getBatchNum() - 1);
                }

            } else {
                if (ContactTracingFeature.enableMultipleWholeBatchSignatureVerification()) {
                    if (!batchChunkMap.containsKey(keyMetadataForMap)) {
                        batchChunkMap.put(keyMetadataForMap, new BitSet(keyMetadata.getBatchSize()));
                    }
                    batchChunkMap.get(keyMetadataForMap).set(0);
                } else {
                    batchChunks.set(0);
                }
            }
        }

        // verify all chunks seen
        if (!ContactTracingFeature.enableMultipleWholeBatchSignatureVerification()
                && batchChunks.nextClearBit(0) < exports.size()) {
            throw new IOException(
                    "missing some chunks out of " + exports.size() + " chunks expected in batch");
        } else {
            SignatureException exceptions = null;
            for (Map.Entry<TemporaryExposureKeyExport, BitSet> entry : batchChunkMap.entrySet()) {
                TemporaryExposureKeyExport keyMetaData = entry.getKey();
                if (keyMetaData.hasBatchSize()
                        && keyMetaData.getBatchSize() != entry.getValue().cardinality()) {
                    SignatureException e =
                            new SignatureException(
                                    "Signature batch size ("
                                            + keyMetaData.getBatchSize()
                                            + " does not match actual batch size ("
                                            + entry.getValue().cardinality()
                                            + ") for batch: "
                                            + createExposureKeyString(keyMetaData));
                    if (exceptions == null) {
                        exceptions = e;
                    } else {
                        exceptions.addSuppressed(e);
                    }
                }
            }
            if (exceptions != null) {
                throw exceptions;
            }
        }

        if (ContactTracingFeature.calculateDiagnosisKeyHash()) {
            keyFilesHash = keyHash.digest();
        }
        Log.log.atInfo().log("Signature verification succeeded");
        return true;
    }

    /**
     * Gets the SHA-256 hash of the key files. If not available, returns an empty array.
     */
    public byte[] getKeyFilesHash() {
        if (keyFilesHash == null) {
            return new byte[0];
        }
        return keyFilesHash;
    }

    public void setDebugPublicKey(@Nullable String debugPublicKey) {
        this.debugPublicKey = debugPublicKey;
    }

    private static final Splitter PUBLIC_KEY_SPLITTER = Splitter.on(':').limit(2);
    private static final Splitter ROTATION_LIST_SPLITTER = Splitter.on('|');

    private static final MapSplitter COSIGNING_LIST_SPLITTER =
            Splitter.on('&').withKeyValueSeparator(',');

    // returns a list of potential keysets where only one keyset within the list must verify - this
    // supports key rotation. within a keyset, all keys within the set must verify - this support
    // cosigning
    private static PartnerPublicKeySets getKeysForPackage(
            String packageName, @Nullable String debugPublicKey)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        // This is the list of all the flag-configured public signing keys, one element for each
        // partner's keys.
        List<String> publicKeyList = ContactTracingFeature.partnerPublicKeys();
        Log.log
                .atFine()
                .log("getKeysForPackage: package name: %s, debug: %s, partner public keys flag: %s",
                        packageName, debugPublicKey, TextUtils.join("\n", publicKeyList));

        // Start with an empty set of keys.
        PartnerPublicKeySets partnerKeySets = new PartnerPublicKeySets(packageName);

        // Parse the flag-defined keys.
        for (String publicKeyInfo : publicKeyList) {
            PartnerPublicKeySets candidateKeySet = parsePublicKeyFlag(publicKeyInfo);
            if (candidateKeySet.packageName.equals(packageName)) {
                Log.log
                        .atInfo()
                        .log("Found flag-defined keys %s for package %s", publicKeyInfo, packageName);
                partnerKeySets.cosignSets.addAll(candidateKeySet.cosignSets);
            }
        }

        // One "extra" public key definition can be set via debug settings. If it is set, and its
        // package is the one we're working with here, we add it as an additional cosign set so that
        // it has an "additive" effect. That is: if the given keyfile's signature passes the debug
        // key's verification, we accept the file, otherwise we continue evaluating the flag-defined
        // public keys.
        if (debugPublicKey != null) {
            Log.log.atInfo().log("Debug key set: %s", debugPublicKey);
            try {
                PartnerPublicKeySets debugKeySet = parsePublicKeyFlag(debugPublicKey);
                if (debugKeySet.packageName.equals(packageName)) {
                    Log.log.atInfo().log("Adding debug key set to partner's sets: %s", packageName);
                    // If there are no flag-defined keys for the package name we're interested, that's OK.
                    // partnerKeySet will be an empty set here, which is fine; we add the debug keys and
                    // continue.
                    partnerKeySets.cosignSets.addAll(debugKeySet.cosignSets);
                }
            } catch (InvalidKeySpecException e) {
                Log.log
                        .atWarning()
                        .withCause(e)
                        .log("Custom diagnosis key signature fields of debug mode is invalid and " +
                                "will be ignored! ");
            }
        }

        return partnerKeySets;
    }

    /**
     * Parse the public key flag value for one partner, e.g.
     * "com.my.package:keyid-v1,<keybytes>&otherid-v1,<otherbytes>|keyid-v2,<morebytes>"
     */
    private static PartnerPublicKeySets parsePublicKeyFlag(String flagValue)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        List<String> parts = PUBLIC_KEY_SPLITTER.splitToList(flagValue);
        String partnerPackageName = parts.get(0);
        PartnerPublicKeySets partnerKeys = new PartnerPublicKeySets(partnerPackageName);

        List<String> partnerKeyConfigs = ROTATION_LIST_SPLITTER.splitToList(parts.get(1));
        for (String cosignSetConfig : partnerKeyConfigs) {
            // Split each cosign config into keys mapped by their name.
            Map<String, String> cosignKeys = COSIGNING_LIST_SPLITTER.split(cosignSetConfig);
            CosignSet cosignSet = new CosignSet();
            for (Map.Entry<String, String> cosignKeyEntry : cosignKeys.entrySet()) {
                byte[] publicKeyBytes = Base64.decode(cosignKeyEntry.getValue(), Base64.DEFAULT);
                cosignSet.keys.put(
                        cosignKeyEntry.getKey(),
                        KeyFactory.getInstance("EC").generatePublic(new X509EncodedKeySpec(publicKeyBytes)));
            }
            partnerKeys.cosignSets.add(cosignSet);
        }

        return partnerKeys;
    }

    private static Verification createCosignatureVerifications(
            TEKSignatureList tekSignatures, CosignSet cosignSet)
            throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        Preconditions.checkArgument(tekSignatures.getSignaturesCount() > 0);
        Preconditions.checkArgument(!cosignSet.keys.isEmpty());

        // if there are more public keys than signatures in the set, the signatures cannot verify
        if (tekSignatures.getSignaturesCount() < cosignSet.keys.size()) {
            return new FailedVerification();
        }

        // enforce that all public keys are matched
        HashSet<String> matchedPublicKeyNames = new HashSet<>();

        // for each signature...
        ImmutableList.Builder<Verification> cosignatureVerificationsBuilder =
                ImmutableList.builderWithExpectedSize(tekSignatures.getSignaturesCount());
        for (TEKSignature tekSignature : tekSignatures.getSignaturesList()) {
            SignatureInfo info = tekSignature.getSignatureInfo();

            // get correct public key to match
            String publicKeyName;
            PublicKey publicKey = null;
            if (info.hasVerificationKeyId()) {
                publicKeyName =
                        info.getVerificationKeyId() + KEY_VERSION_JOINER + info.getVerificationKeyVersion();
                if (cosignSet.keys.containsKey(publicKeyName)) {
                    publicKey = cosignSet.keys.get(publicKeyName);
                }
            } else {
                if (tekSignatures.getSignaturesCount() > 1) {
                    throw new SignatureException(
                            "signature in cosigning group does not specify verification_key_id");
                }
                if (ContactTracingFeature.ignoreUnmatchedSignatures()) {
                    if (cosignSet.keys.size() > 1) {
                        return new FailedVerification();
                    }
                } else {
                    Preconditions.checkState(cosignSet.keys.size() == 1);
                }
                Map.Entry<String, PublicKey> entry = cosignSet.keys.entrySet().iterator().next();
                publicKeyName = entry.getKey();
                publicKey = entry.getValue();
            }

            // if no matching public key found, move on to the next key, otherwise mark key as seen
            if (publicKey == null) {
                if (ContactTracingFeature.ignoreUnmatchedSignatures()) {
                    continue;
                } else {
                    Log.log.atWarning().log("a verification failed due to an unmatched signature (strict)");
                    // if one key in the set cannot be matched, the entire set will fail anyways
                    return new FailedVerification();
                }
            } else if (ContactTracingFeature.ignoreUnmatchedSignatures()) {
                matchedPublicKeyNames.add(publicKeyName);
            }

            // create verifier for public key and add to the list
            String signatureAlgorithmName = OID_TO_NAME_MAP.get(info.getSignatureAlgorithm());
            if (signatureAlgorithmName == null) {
                throw new NoSuchAlgorithmException(
                        "unsupported signature OID: " + info.getSignatureAlgorithm());
            }
            Signature signatureAttempt = Signature.getInstance(signatureAlgorithmName);
            signatureAttempt.initVerify(publicKey);
            cosignatureVerificationsBuilder.add(
                    new SignatureVerification(signatureAttempt, tekSignature.getSignature().toByteArray()));
        }

        // check all public keys are being verified with none left out
        ImmutableList<Verification> verifications = cosignatureVerificationsBuilder.build();
        if (verifications.size() < cosignSet.keys.size()) {
            if (ContactTracingFeature.ignoreUnmatchedSignatures()) {
                Log.log
                        .atInfo()
                        .log(
                                "More public keys in this cosign set than signatures/verifications in this keyfile"
                                        + " (%d > %d). Continuing with other cosign sets.",
                                cosignSet.keys.size(), verifications.size());
                // Failing this cosign set allows us to continue with other cosign sets.
                return new FailedVerification();
            } else {
                // Throwing an exception stops us from attempting signature verification with other cosign
                // sets.
                throw new SignatureException(
                        "expected at least "
                                + cosignSet.keys.size()
                                + " signatures with matching ids, found only "
                                + verifications.size());
            }
        }
        if (ContactTracingFeature.ignoreUnmatchedSignatures()
                && matchedPublicKeyNames.size() != cosignSet.keys.size()) {
            Log.log
                    .atInfo()
                    .log(
                            "Unequal number of signatures/verifications in this keyfile vs number of public keys"
                                    + " in this cosign set. %d != %d. Continuing with other cosign sets.",
                            matchedPublicKeyNames.size(), cosignSet.keys.size());
            // Failing this cosign set (as opposed to throwing an exception) allows us to continue with
            // other cosign sets.
            return new FailedVerification();
        }

        return new CosignatureVerification(verifications);
    }

    private static String createExposureKeyString(TemporaryExposureKeyExport keyMetadata) {
        return "[StartTimestamp="
                + keyMetadata.getStartTimestamp()
                + "; EndTimestamp="
                + keyMetadata.getEndTimestamp()
                + "; Region="
                + keyMetadata.getRegion()
                + "; BatchNum="
                + keyMetadata.getBatchNum()
                + "; BatchSize="
                + keyMetadata.getBatchSize()
                + "]";
    }

    private interface Verification {

        void update(byte[] data, int off, int len) throws SignatureException;

        boolean verify() throws SignatureException;
    }

    /**
     * Individual verification that will immediately fail. Not necessarily indicative that all
     * signature verifications for a file have failed.
     */
    private static class FailedVerification implements Verification {

        @Override
        public void update(byte[] data, int off, int len) {
        }

        @Override
        public boolean verify() {
            return false;
        }
    }

    private static class CosignatureVerification implements Verification {

        private final ImmutableList<Verification> cosignatures;

        private CosignatureVerification(List<Verification> cosignatures) {
            Preconditions.checkArgument(!cosignatures.isEmpty());
            this.cosignatures = ImmutableList.copyOf(cosignatures);
        }

        @Override
        public void update(byte[] data, int off, int len) throws SignatureException {
            for (Verification verification : cosignatures) {
                verification.update(data, off, len);
            }
        }

        @Override
        public boolean verify() throws SignatureException {
            for (Verification verification : cosignatures) {
                if (!verification.verify()) {
                    return false;
                }
            }

            return true;
        }
    }

    private static class SignatureVerification implements Verification {

        private final Signature signature;
        private final byte[] comparison;

        private SignatureVerification(Signature signature, byte[] comparison) {
            this.signature = signature;
            this.comparison = comparison;
        }

        @Override
        public void update(byte[] data, int off, int len) throws SignatureException {
            signature.update(data, off, len);
        }

        @Override
        public boolean verify() throws SignatureException {
            return signature.verify(comparison);
        }
    }

    /**
     * Value class to hold all the public signing keys for one partner's keyfiles.
     *
     * <p>The partner is identified by their app package name, and may have multiple possible sets of
     * keys, called cosign sets. Among a given partner's cosign sets, at least one must successfully
     * verify a keyfile's signature for the keyfile to pass.
     *
     * <p>It is supported for there to be more than one flag value with the same package name, in
     * which case we will concat the lists of CosignSets for each flag so that if any of them pass,
     * the keyfile passes.
     *
     * <p>This is additionally true for public keys supplied via the debug settings: If one is
     * supplied for the same package as flag-defined keys, we'll add the debug key to the list of
     * CosignSets and accept keyfiles which pass its verification.
     */
    private static class PartnerPublicKeySets {

        private final String packageName;
        private final List<CosignSet> cosignSets = new ArrayList<>();

        private PartnerPublicKeySets(String packageName) {
            this.packageName = packageName;
        }
    }

    /**
     * A set of keys that together must all successfully verify a keyfile's signature for the keyfile
     * to pass.
     */
    private static class CosignSet {

        private final Map<String, PublicKey> keys = new HashMap<>();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import com.google.samples.ByteArrayValue;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import static java.nio.charset.StandardCharsets.UTF_8;

/** Encloses constant values related file format definition. */
public final class TemporaryExposureKeyExportV1Header {
    // See https://developers.google.com/android/exposure-notifications/exposure-key-file-format
    public static final ByteArrayValue HEADER =
            ByteArrayValue.from("EK Export v1    ".getBytes(UTF_8)); // right padded with whitespaces

    public static void readAndVerifyHeader(InputStream inputStream) throws IOException {
        byte[] header = new byte[HEADER.length];
        int totalBytesRead = 0;
        if (ContactTracingFeature.readFullMetadataHeader()) {
            int bytesRead = 0;
            while (totalBytesRead < HEADER.length && bytesRead != -1) {
                bytesRead = inputStream.read(header, totalBytesRead, HEADER.length - totalBytesRead);
                totalBytesRead += bytesRead;
            }
        } else {
            totalBytesRead = inputStream.read(header);
        }
        if (totalBytesRead != HEADER.length) {
            throw new IOException(
                    "Invalid header length (read "
                            + totalBytesRead
                            + " bytes, expected "
                            + HEADER.length
                            + " bytes)");
        }
        if (!Arrays.equals(header, HEADER.get())) {
            throw new IOException("Invalid header: " + Arrays.toString(header));
        }
    }

    private TemporaryExposureKeyExportV1Header() {}
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import com.google.protobuf.CodedInputStream;
import com.google.protobuf.ExtensionRegistryLite;
import com.google.protobuf.WireFormat;
import com.google.samples.exposurenotification.ExposureKeyExportProto.SignatureInfo;
import com.google.samples.exposurenotification.ExposureKeyExportProto.TemporaryExposureKeyExport;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * A parser class to parse metadata from exposure key export file.
 */
public class TemporaryExposureKeyFileMetadataParser {
    // An early returning option, should be disabled unless the correctness and robustness is proved
    // when enabled.
    private static final boolean EARLY_RETURN_ENABLED = false;

    /**
     * Parses the metadata (all the fields of {@link TemporaryExposureKeyExport} except the keys). It
     * provides a memory efficient way to get the metadata.
     */
    public static TemporaryExposureKeyExport parse(File file) throws IOException {
        return parse(new FileInputStream(file));
    }

    /**
     * See {@link #parse(File)}
     */
    public static TemporaryExposureKeyExport parse(InputStream inputStream) throws IOException {
        TemporaryExposureKeyExportV1Header.readAndVerifyHeader(inputStream);

        CodedInputStream codedStream = CodedInputStream.newInstance(inputStream);
        TemporaryExposureKeyExport.Builder builder = TemporaryExposureKeyExport.newBuilder();
        int nextTag = codedStream.readTag();
        boolean hasSeenKey = false;
        while (nextTag != 0) {
            switch (WireFormat.getTagFieldNumber(nextTag)) {
                case TemporaryExposureKeyExport.START_TIMESTAMP_FIELD_NUMBER:
                    builder.setStartTimestamp(codedStream.readFixed64());
                    break;
                case TemporaryExposureKeyExport.END_TIMESTAMP_FIELD_NUMBER:
                    builder.setEndTimestamp(codedStream.readFixed64());
                    break;
                case TemporaryExposureKeyExport.REGION_FIELD_NUMBER:
                    builder.setRegion(codedStream.readString());
                    break;
                case TemporaryExposureKeyExport.BATCH_NUM_FIELD_NUMBER:
                    builder.setBatchNum(codedStream.readInt32());
                    break;
                case TemporaryExposureKeyExport.BATCH_SIZE_FIELD_NUMBER:
                    builder.setBatchSize(codedStream.readInt32());
                    break;
                case TemporaryExposureKeyExport.SIGNATURE_INFOS_FIELD_NUMBER:
                    SignatureInfo.Builder signatureBuilder = SignatureInfo.newBuilder();
                    codedStream.readMessage(signatureBuilder, ExtensionRegistryLite.getEmptyRegistry());
                    builder.addSignatureInfos(signatureBuilder);
                    break;
                case TemporaryExposureKeyExport.KEYS_FIELD_NUMBER:
                    hasSeenKey = true;
                    // fall through to skip keys.
                default:
                    // Skip unknown fields.
                    codedStream.skipField(nextTag);
                    break;
            }
            if (EARLY_RETURN_ENABLED && isMetadataComplete(builder, hasSeenKey)) {
                break;
            }
            nextTag = codedStream.readTag();
        }
        return builder.build();
    }

    private static boolean isMetadataComplete(
            TemporaryExposureKeyExport.Builder builder, boolean hasSeenKey) {
        return hasSeenKey
                && builder.hasStartTimestamp()
                && builder.hasEndTimestamp()
                && builder.hasRegion()
                && builder.hasBatchNum()
                && builder.hasBatchSize()
                && builder.getSignatureInfosCount() > 0;
    }

    private TemporaryExposureKeyFileMetadataParser() {
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import com.google.auto.value.AutoValue;

/** A pair of time and attenuation. */
@AutoValue
abstract class TimeAndAttenuation {
    static TimeAndAttenuation create(Integer time, Integer attenuation) {
        return new AutoValue_TimeAndAttenuation(time, attenuation);
    }

    abstract Integer time();

    abstract Integer attenuation();
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.matching;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.auto.value.AutoValue;
import com.google.samples.exposurenotification.features.ContactTracingFeature;
import com.google.samples.exposurenotification.storage.TracingParamsRecord;

import org.joda.time.Duration;

/** Parameters required for {@link ExposureMatchingTracer}. */
@AutoValue
public abstract class TracingParams {

    /**
     * Minimum consecutive sightings in duration required to be considered a contact, compared with a
     * bucketized duration calculation. See {@link ExposureMatchingTracer} for more details. If not
     * set, has default value {@link ContactTracingFeature#minExposureBucketizedDurationSeconds()}
     * converted to {@link Duration}.
     */
    public abstract Duration minExposureBucketizedDuration();

    /**
     * Minimum ble signal strength indicator value that must be reached by at least one sighting in a
     * series of sightings that are being considered for an exposure. If not set, has default value
     * {@link ContactTracingFeature#defaultMinExposureAttenuationValue()} converted to an int.
     */
    // TODO(wukevin): remove this field.
    public abstract int minimumAttenuationValue();

    /**
     * Maximum duration gap between two sightings to be considered continuous contact with another
     * device. If not set, has default value {@link
     * ContactTracingFeature#maxInterpolationDurationSeconds()} converted to {@link Duration}.
     */
    public abstract Duration maxInterpolationDuration();

    /**
     * Bluetooth token scanning interval. If not set, has default value {@link
     * ContactTracingFeature#scanIntervalSeconds()} converted to {@link Duration}.
     */
    public abstract Duration scanInterval();

    /**
     * Boolean controlling the use of interpolated attentuation levels when computing duration at
     * thresholds. If not set, defaults to false.
     *
     * <p>TODO(b/159218245): add ContactTracingFeature flag for a default value.
     */
    public abstract Boolean interpolationEnabled();

    public static Builder builder() {
        return new AutoValue_TracingParams.Builder()
                .setMinExposureBucketizedDuration(
                        Duration.standardSeconds(ContactTracingFeature.minExposureBucketizedDurationSeconds()))
                .setMinimumAttenuationValue(
                        (int) ContactTracingFeature.defaultMinExposureAttenuationValue())
                .setMaxInterpolationDuration(
                        Duration.standardSeconds(ContactTracingFeature.maxInterpolationDurationSeconds()))
                .setScanInterval(Duration.standardSeconds(ContactTracingFeature.scanIntervalSeconds()))
                .setInterpolationEnabled(false);
    }

    public TracingParamsRecord toTracingParamsRecord() {
        return TracingParamsRecord.newBuilder()
                .setMinAttenuationValue(minimumAttenuationValue())
                .setMaxInterpolationDurationSeconds(maxInterpolationDuration().getStandardSeconds())
                .setMinExposureBucketizedDurationSeconds(
                        minExposureBucketizedDuration().getStandardSeconds())
                .setScanIntervalSeconds((int) scanInterval().getStandardSeconds())
                .setInterpolationEnabled(interpolationEnabled())
                .build();
    }

    /** Builder clas for {@link TracingParams}. */
    @AutoValue.Builder
    public abstract static class Builder {
        public abstract Builder setMinExposureBucketizedDuration(
                Duration minExposureBucketizedDuration);

        public abstract Builder setMinimumAttenuationValue(int minimumAttenuationValue);

        public abstract Builder setMaxInterpolationDuration(Duration maxInterpolationDuration);

        public abstract Builder setScanInterval(Duration scanInterval);

        public abstract Builder setInterpolationEnabled(boolean interpolationEnabled);

        public abstract TracingParams autoBuild();

        public TracingParams build() {
            TracingParams tracingParams = autoBuild();
            // Use reverse logic to get >= equivalent from org.joda.time.Duration api.
            checkArgument(!tracingParams.minExposureBucketizedDuration().isShorterThan(Duration.ZERO));
            checkArgument(tracingParams.minimumAttenuationValue() >= 0);
            checkArgument(tracingParams.scanInterval().isLongerThan(Duration.ZERO));
            checkArgument(tracingParams.maxInterpolationDuration().isLongerThan(Duration.ZERO));
            return tracingParams;
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.Nullable;

import com.google.common.base.Preconditions;
import com.google.common.primitives.Bytes;
import com.google.samples.exposurenotification.ble.utils.Constants;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

/**
 * Fully constructed service data that should be advertised over BLE.
 */
public class AdvertisementPacket {
    private final byte[] id;
    private final byte[] metadata;
    private final byte[] encryptedMetadata;

    private AdvertisementPacket(byte[] id, byte[] metadata, byte[] encryptedMetadata) {
        this.id = id;
        this.metadata = metadata;
        this.encryptedMetadata = encryptedMetadata;
    }

    /**
     * Retrieves fully constructed service data.
     */
    public byte[] getPacket() {
        return Bytes.concat(id, encryptedMetadata);
    }

    /**
     * Returns the rolling proximity ID.
     *
     * <p>This method returns the mutable reference to the byte array. Use with caution!
     */
    public byte[] getIdDirect() {
        return id;
    }

    /**
     * Returns the raw (unencrypted) metadata for debugging purpose.
     *
     * <p>This method returns the mutable reference to the byte array. Use with caution!
     */
    public byte[] getRawMetadataDirect() {
        return metadata;
    }

    /**
     * Returns the encrypted metadata.
     *
     * <p>This method returns the mutable reference to the byte array. Use with caution!
     */
    public byte[] getEncryptedMetadataDirect() {
        return encryptedMetadata;
    }

    @Override
    public String toString() {
        return "ID: "
                + Constants.fromIdByteArrayToString(id)
                + " Metadata: "
                + Constants.fromIdByteArrayToString(encryptedMetadata);
    }

    /**
     * Builder that assists with constructing valid AdvertisementPackets.
     */
    public static class Builder {
        @Nullable
        private byte[] id;
        @Nullable
        private byte[] metadata;
        @Nullable
        private byte[] encryptedMetadata;

        /**
         * Sets the rotating proximity ID that should be put into the advertisement packet.
         *
         * @throws IllegalArgumentException If the {@code id} is not the right length.
         */
        public Builder setProxId(byte[] id) {
            Preconditions.checkArgument(
                    id.length == ContactTracingFeature.contactIdLength(),
                    "Invalid prox ID length %s",
                    id.length);

            this.id = id;

            return this;
        }

        /**
         * Sets the raw metadata.
         *
         * <p>Raw metadata is for debugging purpose only. It's not put into the advertisement packet.
         *
         * @throws IllegalArgumentException if the {@code metadata} is not the right length.
         */
        public Builder setRawMetadata(byte[] metadata) {
            Preconditions.checkArgument(
                    metadata.length == Constants.METADATA_LENGTH,
                    "Invalid metadata length %s",
                    metadata.length);

            this.metadata = metadata;
            return this;
        }

        /**
         * Sets the encrypted metadata that should be put into the advertisement packet.
         *
         * @throws IllegalArgumentException if the {@code encryptedMetadata} is not the right length.
         */
        public Builder setEncryptedMetadata(byte[] encryptedMetadata) {
            Preconditions.checkArgument(
                    encryptedMetadata.length == Constants.METADATA_LENGTH,
                    "Invalid encryptedMetadata length %s",
                    encryptedMetadata.length);

            this.encryptedMetadata = encryptedMetadata;
            return this;
        }

        /**
         * Constructs an advertisement packet.
         */
        public AdvertisementPacket build() {
            Preconditions.checkNotNull(id, "id must be set.");
            Preconditions.checkNotNull(metadata, "metadata must be set.");
            Preconditions.checkNotNull(encryptedMetadata, "encryptedMetadata must be set.");
            return new AdvertisementPacket(id, metadata, encryptedMetadata);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.IntDef;

import com.google.samples.exposurenotification.ExposureNotificationEnums;

/**
 * Calibration confidence defined for an {@link ExposureWindow}.
 */
@IntDef({
        CalibrationConfidence.LOWEST,
        CalibrationConfidence.LOW,
        CalibrationConfidence.MEDIUM,
        CalibrationConfidence.HIGH,
})
public @interface CalibrationConfidence {
    /**
     * No calibration data, using fleet-wide as default options.
     */
    int LOWEST = ExposureNotificationEnums.CalibrationConfidence.LOWEST_CONFIDENCE_VALUE;

    /**
     * Using average calibration over models from manufacturer.
     */
    int LOW = ExposureNotificationEnums.CalibrationConfidence.LOW_CONFIDENCE_VALUE;

    /**
     * Using single-antenna orientation for a similar model.
     */
    int MEDIUM = ExposureNotificationEnums.CalibrationConfidence.MEDIUM_CONFIDENCE_VALUE;

    /**
     * Using significant calibration data for this model.
     */
    int HIGH = ExposureNotificationEnums.CalibrationConfidence.HIGH_CONFIDENCE_VALUE;
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.FloatRange;
import androidx.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.samples.exposurenotification.ExposureNotificationEnums;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * Configuration of per-day summary of exposures.
 *
 * <p>During summarization the following are computed for each {@link ExposureWindow}s:
 *
 * <ul>
 *   <li>a weighted duration, computed as
 *       <p><code>
 *          ( immediateDurationSeconds * immediateDurationWeight ) +
 *          ( nearDurationSeconds      * nearDurationWeight ) +
 *          ( mediumDurationSeconds    * mediumDurationWeight ) +
 *          ( otherDurationSeconds     * otherDurationWeight )
 *       </code>
 *   <li>a score, computed as
 *       <p><code>
 *          reportTypeWeights[Tek.reportType] *
 *          infectiousnessWeights[infectiousness] *
 *          weightedDuration
 *       </code>
 *       <p>where infectiousness and reportType are set based on the ExposureWindow's diagnosis key
 *       and the DiagnosisKeysDataMapping
 * </ul>
 *
 * <p>The {@link ExposureWindow}s are then filtered, removing those with score lower than {@link
 * #getMinimumWindowScore()}.
 *
 * <p>Scores and weighted durations of the {@link ExposureWindow}s that pass the {@link
 * #getMinimumWindowScore()} are then aggregated over a day to compute the maximum and cumulative
 * scores and duration:
 *
 * <ul>
 *   <li>sumScore = sum(score of ExposureWindows)
 *   <li>maxScore = max(score of ExposureWindows)
 *   <li>weightedDurationSum = sum(weighted duration of ExposureWindow)
 * </ul>
 *
 * <p>Note that when the weights are typically around 100% (1.0), both the scores and the
 * weightedDurationSum can be considered as being expressed in seconds. For example, 15 minutes of
 * exposure with all weights equal to 1.0 would be 60 * 15 = 900 (seconds).
 */
public class DailySummariesConfig {

    List<Double> reportTypeWeights;
    List<Double> infectiousnessWeights;
    List<Integer> attenuationBucketThresholdDb;
    List<Double> attenuationBucketWeights;
    int daysSinceExposureThreshold;
    double minimumWindowScore;

    DailySummariesConfig(
            List<Double> reportTypeWeights,
            List<Double> infectiousnessWeights,
            List<Integer> attenuationBucketThresholdDb,
            List<Double> attenuationBucketWeights,
            int daysSinceExposureThreshold,
            double minimumWindowScore) {
        this.reportTypeWeights = reportTypeWeights;
        this.infectiousnessWeights = infectiousnessWeights;
        this.attenuationBucketThresholdDb = attenuationBucketThresholdDb;
        this.attenuationBucketWeights = attenuationBucketWeights;
        this.daysSinceExposureThreshold = daysSinceExposureThreshold;
        this.minimumWindowScore = minimumWindowScore;
    }

    /**
     * Scoring weights to associate with exposures with different {@link ReportType}s.
     *
     * <p>This map can include weights for the following {@link ReportType}s:
     *
     * <ul>
     *   <li>CONFIRMED_TEST
     *   <li>CONFIRMED_CLINICAL_DIAGNOSIS
     *   <li>SELF_REPORT
     *   <li>RECURSIVE (reserved for future use)
     * </ul>
     * <p>
     * Each element must be between 0 and 2.5.
     */
    public Map<Integer, Double> getReportTypeWeights() {
        HashMap<Integer, Double> result = new HashMap<>();
        for (int i = 0; i < reportTypeWeights.size(); i++) {
            result.put(i, reportTypeWeights.get(i));
        }
        return result;
    }

    /**
     * Scoring weights to associate with exposures with different {@link Infectiousness}.
     *
     * <p>This map can include weights for the following {@link Infectiousness} values:
     *
     * <ul>
     *   <li>STANDARD
     *   <li>HIGH
     * </ul>
     * <p>
     * Each element must be between 0 and 2.5.
     */
    public Map<Integer, Double> getInfectiousnessWeights() {
        HashMap<Integer, Double> result = new HashMap<>();
        for (int i = 0; i < infectiousnessWeights.size(); i++) {
            result.put(i, infectiousnessWeights.get(i));
        }
        return result;
    }

    /**
     * Thresholds defining the BLE attenuation buckets edges.
     *
     * <p>This list must have 3 elements: the immediate, near, and medium thresholds. See
     * attenuationBucketWeights for more information.
     *
     * <p>These elements must be between 0 and 255 and come in increasing order.
     */
    public List<Integer> getAttenuationBucketThresholdDb() {
        return new ArrayList<>(attenuationBucketThresholdDb);
    }

    /**
     * Scoring weights to associate with ScanInstances depending on the attenuation bucket in which
     * their typicalAttenuationDb falls.
     *
     * <p>This list must have 4 elements, corresponding to the weights for the 4 buckets.
     *
     * <ul>
     *   <li>immediate bucket: -infinity < attenuation <= immediate threshold
     *   <li>near bucket: immediate threshold < attenuation <= near threshold
     *   <li>medium bucket: near threshold < attenuation <= medium threshold
     *   <li>other bucket: medium threshold < attenuation < +infinity
     * </ul>
     * <p>
     * Each element must be between 0 and 2.5.
     */
    public List<Double> getAttenuationBucketWeights() {
        return new ArrayList<>(attenuationBucketWeights);
    }

    /**
     * Day summaries older than this are not returned.
     *
     * <p>To return all available day summaries, set to 0, which is treated specially.
     */
    public int getDaysSinceExposureThreshold() {
        return daysSinceExposureThreshold;
    }

    /**
     * Minimum score that ExposureWindows must reach in order to be included in the {@link
     * DailySummary.ExposureSummaryData}.
     *
     * <p>Use 0 to consider all {@link ExposureWindow}s (recommended).
     */
    public double getMinimumWindowScore() {
        return minimumWindowScore;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof DailySummariesConfig) {
            DailySummariesConfig that = (DailySummariesConfig) obj;
            return reportTypeWeights.equals(that.reportTypeWeights)
                    && infectiousnessWeights.equals(that.infectiousnessWeights)
                    && attenuationBucketThresholdDb.equals(that.attenuationBucketThresholdDb)
                    && attenuationBucketWeights.equals(that.attenuationBucketWeights)
                    && daysSinceExposureThreshold == that.daysSinceExposureThreshold
                    && minimumWindowScore == that.minimumWindowScore;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(
                reportTypeWeights,
                infectiousnessWeights,
                attenuationBucketThresholdDb,
                attenuationBucketWeights,
                daysSinceExposureThreshold,
                minimumWindowScore);
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "DailySummariesConfig<"
                        + "reportTypeWeights: %s, "
                        + "infectiousnessWeights: %s, "
                        + "attenuationBucketThresholdDb: %s, "
                        + "attenuationBucketWeights: %s"
                        + "daysSinceExposureThreshold: %d,"
                        + "minimumWindowScore: %.3f>",
                reportTypeWeights,
                infectiousnessWeights,
                attenuationBucketThresholdDb,
                attenuationBucketWeights,
                daysSinceExposureThreshold,
                minimumWindowScore);
    }

    /**
     * Definition of attenuation bucket group.
     *
     * @hide
     */
    public enum DistanceEstimate {
        IMMEDIATE,
        NEAR,
        MEDIUM,
        OTHER;

        public static DistanceEstimate getDistanceEstimate(
                int typicalAttenuation, DailySummariesConfig config) {
            if (typicalAttenuation <= config.getAttenuationBucketThresholdDb().get(0)) {
                return IMMEDIATE;
            } else if (typicalAttenuation <= config.getAttenuationBucketThresholdDb().get(1)) {
                return NEAR;
            } else if (typicalAttenuation <= config.getAttenuationBucketThresholdDb().get(2)) {
                return MEDIUM;
            } else {
                return OTHER;
            }
        }
    }

    /**
     * A builder for {@link DailySummariesConfig}.
     */
    public static final class DailySummariesConfigBuilder {
        private Double[] reportTypeWeights = new Double[6]; // 6 = number of ReportType values.
        private Double[] infectiousnessWeights =
                new Double[ExposureNotificationEnums.Infectiousness.values().length];
        private List<Integer> attenuationBucketThresholdDb;
        private List<Double> attenuationBucketWeights;
        int daysSinceExposureThreshold = 0;
        double minimumWindowScore = 0;

        public DailySummariesConfigBuilder() {
            Arrays.fill(reportTypeWeights, 0.0);
            Arrays.fill(infectiousnessWeights, 0.0);
        }

        /**
         * See {@link DailySummariesConfig#getReportTypeWeights()}.
         */
        public DailySummariesConfigBuilder setReportTypeWeight(
                @ReportType int reportType, @FloatRange(from = 0.0, to = 2.5) double weight) {
            checkArgument(
                    reportType >= ReportType.UNKNOWN && reportType <= ReportType.REVOKED,
                    "Incorrect value of reportType");
            checkSingleWeightRange(weight, "reportTypeWeights");
            this.reportTypeWeights[reportType] = weight;
            return this;
        }

        /**
         * See {@link DailySummariesConfig#getInfectiousnessWeights()}
         */
        public DailySummariesConfigBuilder setInfectiousnessWeight(
                @Infectiousness int infectiousness, @FloatRange(from = 0.0, to = 2.5) double weight) {
            checkArgument(
                    ExposureNotificationEnums.Infectiousness.forNumber(infectiousness) != null,
                    "Incorrect value of infectiousness");
            checkSingleWeightRange(weight, "infectiousnessWeights");
            this.infectiousnessWeights[infectiousness] = weight;
            return this;
        }

        /**
         * See {@link DailySummariesConfig#getAttenuationBucketThresholdDb()} and {@link
         * DailySummariesConfig#getAttenuationBucketWeights()} ()}
         */
        public DailySummariesConfigBuilder setAttenuationBuckets(
                List<Integer> thresholds, List<Double> weights) {
            setAttenuationBucketThreshold(thresholds);
            setAttenuationBucketWeights(weights);
            return this;
        }

        private void setAttenuationBucketThreshold(List<Integer> attenuationBucketThresholdDb) {
            checkListSize(
                    attenuationBucketThresholdDb,
                    DistanceEstimate.values().length - 1,
                    "attenuationBucketThresholdDb");
            for (int i = 0; i < attenuationBucketThresholdDb.size(); i++) {
                checkArgument(
                        attenuationBucketThresholdDb.get(i) >= 0 && attenuationBucketThresholdDb.get(i) <= 255,
                        "Element of attenuationBucketThreshold must between 0 ~ 255");
                if (i == 0) {
                    continue;
                }
                checkArgument(
                        attenuationBucketThresholdDb.get(i - 1) < attenuationBucketThresholdDb.get(i),
                        String.format(
                                Locale.ENGLISH,
                                "attenuationBucketThresholdDb of index %d must be larger than index %d",
                                i,
                                i - 1));
            }
            this.attenuationBucketThresholdDb = new ArrayList<>(attenuationBucketThresholdDb);
        }

        private void setAttenuationBucketWeights(List<Double> attenuationBucketWeights) {
            checkListSize(
                    attenuationBucketWeights, DistanceEstimate.values().length, "attenuationBucketWeights");
            checkWeightRange(attenuationBucketWeights, "attenuationBucketWeights");
            this.attenuationBucketWeights = new ArrayList<>(attenuationBucketWeights);
        }

        /**
         * See {@link DailySummariesConfig#getDaysSinceExposureThreshold()}
         */
        public DailySummariesConfigBuilder setDaysSinceExposureThreshold(
                int daysSinceExposureThreshold) {
            checkArgument(
                    daysSinceExposureThreshold >= 0, "daysSinceExposureThreshold must not be negative");
            this.daysSinceExposureThreshold = daysSinceExposureThreshold;
            return this;
        }

        /**
         * See {@link DailySummariesConfig#getMinimumWindowScore()}
         */
        public DailySummariesConfigBuilder setMinimumWindowScore(double minimumWindowScore) {
            checkArgument(minimumWindowScore >= 0, "minimumWindowScore must not be negative");
            this.minimumWindowScore = minimumWindowScore;
            return this;
        }

        public DailySummariesConfig build() {
            checkArgument(attenuationBucketThresholdDb != null, "Must set attenuationBucketThresholdDb");
            checkArgument(attenuationBucketWeights != null, "Must set attenuationBucketWeights");
            return new DailySummariesConfig(
                    Arrays.asList(reportTypeWeights),
                    Arrays.asList(infectiousnessWeights),
                    attenuationBucketThresholdDb,
                    attenuationBucketWeights,
                    daysSinceExposureThreshold,
                    minimumWindowScore);
        }

        @SuppressWarnings("rawtypes")
        private static void checkListSize(List list, int expectSize, String name) {
            checkArgument(list != null, String.format(Locale.ENGLISH, "%s must not be null", name));
            checkArgument(
                    list.size() == expectSize,
                    String.format(Locale.ENGLISH, "%s must must contains %d elements", name, expectSize));
        }

        private static void checkWeightRange(List<Double> values, String name) {
            for (double value : values) {
                checkSingleWeightRange(value, name);
            }
        }

        private static void checkSingleWeightRange(double value, String name) {
            checkArgument(
                    value >= 0 && value <= 2.5,
                    String.format(Locale.ENGLISH, "Element value of %s must between 0 ~ 2.5", name));
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import android.os.Parcel;

import androidx.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.samples.exposurenotification.annotations.Hide;
import com.google.samples.exposurenotification.nearby.DailySummary.ExposureSummaryData.ExposureSummaryDataBuilder;
import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
import com.google.samples.exposurenotification.safeparcel.ReflectedParcelable;
import com.google.samples.exposurenotification.safeparcel.SafeParcelable;

import java.util.Arrays;
import java.util.List;
import java.util.Locale;


/** Daily exposure summary to pass to client side. */
@SafeParcelable.Class(creator = "DailySummaryCreator")
public class DailySummary extends AbstractSafeParcelable implements ReflectedParcelable {

    @Field(id = 1, getter = "getDaysSinceEpoch")
    int daysSinceEpoch;

    @Field(id = 2, getter = "getReportSummaries")
    List<ExposureSummaryData> reportSummaries;

    @Field(id = 3, getter = "getSummaryData")
    ExposureSummaryData summaryData;

    @Constructor
    DailySummary(
            @Param(id = 1) int daysSinceEpoch,
            @Param(id = 2) List<ExposureSummaryData> reportSummaries,
            @Param(id = 3) ExposureSummaryData summaryData) {
        this.daysSinceEpoch = daysSinceEpoch;
        this.reportSummaries = reportSummaries;
        this.summaryData = summaryData;
    }

    /** Returns days since epoch of the {@link ExposureWindow}s that went into this summary. */
    public int getDaysSinceEpoch() {
        return daysSinceEpoch;
    }

    /** Summary of all exposures on this day of a specific diagnosis {@link ReportType}. */
    public ExposureSummaryData getSummaryDataForReportType(@ReportType int reportType) {
        return reportSummaries.get(reportType);
    }

    /** For {@link #CREATOR} to serialize {@link #reportSummaries}. */
    List<ExposureSummaryData> getReportSummaries() {
        return reportSummaries;
    }

    /** Summary of all exposures on this day. */
    public ExposureSummaryData getSummaryData() {
        return summaryData;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof DailySummary) {
            DailySummary that = (DailySummary) obj;
            return daysSinceEpoch == that.daysSinceEpoch
                    && reportSummaries.equals(that.reportSummaries)
                    && Objects.equal(summaryData, that.summaryData);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(daysSinceEpoch, reportSummaries, summaryData);
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "DailySummary<" + "daysSinceEpoch: %d, " + "reportSummaries: %s, " + "daySummary: %s>",
                daysSinceEpoch,
                reportSummaries,
                summaryData);
    }

    /**
     * A builder for {@link DailySummary}.
     *
     * @hide
     */
    @Hide
    public static final class DailySummaryBuilder {
        private int daysSinceEpoch = 0;
        private ExposureSummaryData[] reportSummaries =
                new ExposureSummaryData[6]; // 6 = number of possible ReportTypes.
        private ExposureSummaryData summaryData;

        public DailySummaryBuilder() {
            Arrays.fill(reportSummaries, new ExposureSummaryDataBuilder().build());
        }

        public DailySummaryBuilder setDaysSinceEpoch(int daysSinceEpoch) {
            this.daysSinceEpoch = daysSinceEpoch;
            return this;
        }

        public DailySummaryBuilder setReportSummary(
                @ReportType int reportType, ExposureSummaryData reportTypeSummaryData) {
            this.reportSummaries[reportType] = reportTypeSummaryData;
            return this;
        }

        public DailySummaryBuilder setSummaryData(ExposureSummaryData summaryData) {
            this.summaryData = summaryData;
            return this;
        }

        public DailySummary build() {
            return new DailySummary(daysSinceEpoch, Arrays.asList(reportSummaries), summaryData);
        }
    }

    /** Stores different scores for specific {@link ReportType}. */
    @SafeParcelable.Class(creator = "ExposureSummaryDataCreator")
    public static class ExposureSummaryData extends AbstractSafeParcelable
            implements ReflectedParcelable {

        @Field(id = 1, getter = "getMaximumScore")
        double maximumScore;

        @Field(id = 2, getter = "getScoreSum")
        double scoreSum;

        @Field(id = 3, getter = "getWeightedDurationSum")
        double weightedDurationSum;

        @Constructor
        ExposureSummaryData(
                @Param(id = 1) double maximumScore,
                @Param(id = 2) double scoreSum,
                @Param(id = 3) double weightedDurationSum) {
            this.maximumScore = maximumScore;
            this.scoreSum = scoreSum;
            this.weightedDurationSum = weightedDurationSum;
        }

        /**
         * Highest score of all {@link ExposureWindow}s aggregated into this summary.
         *
         * <p>See {@link DailySummariesConfig} for more information about how the per-ExposureWindow
         * score is computed.
         */
        public double getMaximumScore() {
            return maximumScore;
        }

        /**
         * Sum of scores for all {@link ExposureWindow}s aggregated into this summary.
         *
         * <p>See {@link DailySummariesConfig} for more information about how the per-ExposureWindow
         * score is computed.
         */
        public double getScoreSum() {
            return scoreSum;
        }

        /**
         * Sum of weighted durations for all {@link ExposureWindow}s aggregated into this summary.
         *
         * <p>See {@link DailySummariesConfig} for more information about how the per-{@link
         * ExposureWindow} score and weightedDurationSum are computed.
         */
        public double getWeightedDurationSum() {
            return weightedDurationSum;
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (obj instanceof ExposureSummaryData) {
                ExposureSummaryData that = (ExposureSummaryData) obj;
                return maximumScore == that.maximumScore
                        && scoreSum == that.scoreSum
                        && weightedDurationSum == that.weightedDurationSum;
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(maximumScore, scoreSum, weightedDurationSum);
        }

        @Override
        public String toString() {
            return String.format(
                    Locale.US,
                    "ExposureSummaryData<"
                            + "maximumScore: %.3f, "
                            + "scoreSum: %.3f, "
                            + "weightedDurationSum: %.3f>",
                    maximumScore,
                    scoreSum,
                    weightedDurationSum);
        }

        /** A builder for {@link ExposureSummaryData}. @hide */
        @Hide
        public static final class ExposureSummaryDataBuilder {
            private double maximumScore = 0;
            private double scoreSum = 0;
            private double weightedDurationSum = 0;

            public ExposureSummaryDataBuilder setMaximumScore(double maximumScore) {
                this.maximumScore = maximumScore;
                return this;
            }

            public ExposureSummaryDataBuilder setScoreSum(double scoreSum) {
                this.scoreSum = scoreSum;
                return this;
            }

            public ExposureSummaryDataBuilder setWeightedDurationSum(double weightedDurationSum) {
                this.weightedDurationSum = weightedDurationSum;
                return this;
            }

            public ExposureSummaryData build() {
                return new ExposureSummaryData(maximumScore, scoreSum, weightedDurationSum);
            }
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import com.google.common.collect.ImmutableList;
import java.io.File;
import java.util.List;

/**
 * Provider which holds a list of diagnosis key files and can open/supply them one by one as they
 * are ready to be processed.
 */
public class DiagnosisKeyFileProvider {

    private int index = 0;
    private final List<File> files;

    public DiagnosisKeyFileProvider(List<File> files) {
        this.files = ImmutableList.copyOf(files);
    }

    /**
     * Checks whether another File is available.
     *
     * @hide
     */
    public boolean hasNext() {
        return files.size() > index;
    }

    /**
     * Gets the current file and increments the pointer to the next.
     *
     * @hide
     */
    public File getNext() {
        return files.get(index++);
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.samples.exposurenotification.ExposureNotificationEnums;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import static com.google.common.base.Preconditions.checkArgument;

/** Mappings from diagnosis keys data to concepts returned by the API. */
public class DiagnosisKeysDataMapping {
    /**
     * The maximum days since onset for checking the valid range of {@link
     * #getDaysSinceOnsetToInfectiousness}.
     *
     * @hide
     */
    public static final int MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET = 14;

    /**
     * The maximum size of {@link #getDaysSinceOnsetToInfectiousness}.
     *
     * @hide
     */
    public static final int SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS =
            MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET * 2 + 1;

    List<Integer> daysSinceOnsetToInfectiousness;

    @ReportType
    int reportTypeWhenMissing;

    @Infectiousness
    int infectiousnessWhenDaysSinceOnsetMissing;

    DiagnosisKeysDataMapping(
            List<Integer> daysSinceOnsetToInfectiousness,
            @ReportType int reportTypeWhenMissing,
            @Infectiousness int infectiousnessWhenDaysSinceOnsetMissing) {
        this.daysSinceOnsetToInfectiousness = daysSinceOnsetToInfectiousness;
        this.reportTypeWhenMissing = reportTypeWhenMissing;
        this.infectiousnessWhenDaysSinceOnsetMissing = infectiousnessWhenDaysSinceOnsetMissing;
    }

    /**
     * Mapping from diagnosisKey.daysSinceOnsetOfSymptoms to {@link Infectiousness}.
     *
     * <p>Infectiousness is computed from this mapping and the tek metadata as -
     * daysSinceOnsetToInfectiousness[{@link TemporaryExposureKey#getDaysSinceOnsetOfSymptoms}], or -
     * {@link #getInfectiousnessWhenDaysSinceOnsetMissing} if {@link
     * TemporaryExposureKey#getDaysSinceOnsetOfSymptoms} is {@link
     * TemporaryExposureKey#DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN}.
     *
     * <p>Values of DaysSinceOnsetOfSymptoms that aren't represented in this map are given {@link
     * Infectiousness#NONE} as infectiousness. Exposures with infectiousness equal to {@link
     * Infectiousness#NONE} are dropped.
     */
    public Map<Integer, Integer> getDaysSinceOnsetToInfectiousness() {
        HashMap<Integer, Integer> map = Maps.newHashMapWithExpectedSize(29);
        for (int i = 0; i < daysSinceOnsetToInfectiousness.size(); i++) {
            map.put(i - 14, daysSinceOnsetToInfectiousness.get(i));
        }
        return map;
    }

    /**
     * Report type to default to when a TEK has no report type set.
     *
     * <p>This report type gets used when creating the {@link ExposureWindow}s and the {@link
     * DailySummary}s. The system will treat TEKs with missing report types as if they had this
     * provided report type.
     */
    @ReportType
    public int getReportTypeWhenMissing() {
        return reportTypeWhenMissing;
    }

    /**
     * Infectiousness of TEKs for which onset of symptoms is not set.
     *
     * <p>See {@link #getDaysSinceOnsetToInfectiousness} for more info.
     */
    @Infectiousness
    public int getInfectiousnessWhenDaysSinceOnsetMissing() {
        return infectiousnessWhenDaysSinceOnsetMissing;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof DiagnosisKeysDataMapping) {
            DiagnosisKeysDataMapping that = (DiagnosisKeysDataMapping) obj;
            return daysSinceOnsetToInfectiousness.equals(that.daysSinceOnsetToInfectiousness)
                    && reportTypeWhenMissing == that.reportTypeWhenMissing
                    && infectiousnessWhenDaysSinceOnsetMissing
                    == that.infectiousnessWhenDaysSinceOnsetMissing;
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(
                daysSinceOnsetToInfectiousness,
                reportTypeWhenMissing,
                infectiousnessWhenDaysSinceOnsetMissing);
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "DiagnosisKeysDataMapping<"
                        + "daysSinceOnsetToInfectiousness: %s, "
                        + "reportTypeWhenMissing: %d, "
                        + "infectiousnessWhenDaysSinceOnsetMissing: %d>",
                getDaysSinceOnsetToInfectiousness(),
                reportTypeWhenMissing,
                infectiousnessWhenDaysSinceOnsetMissing);
    }

    /** A builder for {@link DiagnosisKeysDataMapping}. */
    public static final class DiagnosisKeysDataMappingBuilder {
        private List<Integer> daysSinceOnsetToInfectiousness = null;

        @ReportType private int reportTypeWhenMissing = ReportType.UNKNOWN;

        @Infectiousness private Integer infectiousnessWhenDaysSinceOnsetMissing = null;

        /**
         * For internal converting the data back to API surface data structure.
         *
         * @hide
         */
        public DiagnosisKeysDataMappingBuilder internalSetDaysSinceOnsetToInfectiousness(
                List<ExposureNotificationEnums.Infectiousness> daysSinceOnsetToInfectiousness) {
            checkArgument(
                    daysSinceOnsetToInfectiousness.size() == SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS,
                    "The size of daysSinceOnsetToInfectiousness must be %d.",
                    SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS);

            this.daysSinceOnsetToInfectiousness =
                    Lists.newArrayListWithExpectedSize(SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS);
            for (ExposureNotificationEnums.Infectiousness infectiousness :
                    daysSinceOnsetToInfectiousness) {
                this.daysSinceOnsetToInfectiousness.add(infectiousness.getNumber());
            }
            return this;
        }

        public DiagnosisKeysDataMappingBuilder setDaysSinceOnsetToInfectiousness(
                Map<Integer, Integer> daysSinceOnsetToInfectiousness) {
            checkArgument(
                    daysSinceOnsetToInfectiousness != null,
                    "daysSinceOnsetToInfectiousness must not be null.");
            checkArgument(
                    daysSinceOnsetToInfectiousness.size() <= SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS,
                    "the size of daysSinceOnsetToInfectiousness exceeds maximum size %d.",
                    SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS);

            Integer[] infectiousnessArray = new Integer[SIZE_OF_DAYS_SINCE_ONSET_TO_INFECTIOUSNESS];
            Arrays.fill(infectiousnessArray, Infectiousness.NONE);
            for (Map.Entry<Integer, Integer> entry : daysSinceOnsetToInfectiousness.entrySet()) {
                int daysSinceOnset = entry.getKey();
                int infectiousness = entry.getValue();
                checkArgument(
                        Math.abs(daysSinceOnset) <= MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET,
                        "Invalid day since onset %d",
                        daysSinceOnset);
                checkArgument(
                        ExposureNotificationEnums.Infectiousness.forNumber(infectiousness) != null,
                        "Invalid value of Infectiousness %d",
                        infectiousness);
                infectiousnessArray[MAXIMUM_OFFSET_OF_DAYS_SINCE_ONSET + daysSinceOnset] = infectiousness;
            }
            this.daysSinceOnsetToInfectiousness = Arrays.asList(infectiousnessArray);
            return this;
        }

        public DiagnosisKeysDataMappingBuilder setReportTypeWhenMissing(
                @ReportType int reportTypeWhenMissing) {
            checkArgument(
                    reportTypeWhenMissing != ReportType.UNKNOWN, "Invalid reportTypeWhenMissing value");
            checkArgument(
                    reportTypeWhenMissing >= ReportType.UNKNOWN
                            && reportTypeWhenMissing <= ReportType.REVOKED,
                    "Invalid value of ReportType %d",
                    reportTypeWhenMissing);
            this.reportTypeWhenMissing = reportTypeWhenMissing;
            return this;
        }

        public DiagnosisKeysDataMappingBuilder setInfectiousnessWhenDaysSinceOnsetMissing(
                @Infectiousness int infectiousnessWhenDaysSinceOnsetMissing) {
            checkArgument(
                    ExposureNotificationEnums.Infectiousness.forNumber(
                            infectiousnessWhenDaysSinceOnsetMissing)
                            != null,
                    "Invalid value of Infectiousness %d",
                    infectiousnessWhenDaysSinceOnsetMissing);
            this.infectiousnessWhenDaysSinceOnsetMissing = infectiousnessWhenDaysSinceOnsetMissing;
            return this;
        }

        public DiagnosisKeysDataMapping build() {
            checkArgument(
                    daysSinceOnsetToInfectiousness != null, "Must set daysSinceOnsetToInfectiousness");
            checkArgument(reportTypeWhenMissing != ReportType.UNKNOWN, "Must set reportTypeWhenMissing");
            checkArgument(
                    infectiousnessWhenDaysSinceOnsetMissing != null,
                    "Must set infectiousnessWhenDaysSinceOnsetMissing");
            return new DiagnosisKeysDataMapping(
                    daysSinceOnsetToInfectiousness,
                    reportTypeWhenMissing,
                    infectiousnessWhenDaysSinceOnsetMissing);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
import com.google.samples.exposurenotification.safeparcel.SafeParcelable;

import java.util.Arrays;
import java.util.Locale;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * Exposure configuration parameters that can be provided when initializing the service.
 *
 * <p>These parameters are used to calculate risk for each exposure incident using the following
 * formula:
 *
 * <p><code>
 *   RiskScore = attenuationScore
 *       * daysSinceLastExposureScore
 *       * durationScore
 *       * transmissionRiskScore
 * </code>
 *
 * <p>Scores are in the range [0-8]. Weights are in the range [0-100].
 *
 * @deprecated no longer used with Exposure Windows API.
 */
@SafeParcelable.Class(creator = "ExposureConfigurationCreator")
@Deprecated
public final class ExposureConfiguration extends AbstractSafeParcelable {

    @Field(id = 1, getter = "getMinimumRiskScore")
    int minimumRiskScore;

    @Field(id = 2, getter = "getAttenuationScores")
    int[] attenuationScores;

    @Field(id = 3, getter = "getAttenuationWeight")
    int attenuationWeight;

    @Field(id = 4, getter = "getDaysSinceLastExposureScores")
    int[] daysSinceLastExposureScores;

    @Field(id = 5, getter = "getDaysSinceLastExposureWeight")
    int daysSinceLastExposureWeight;

    @Field(id = 6, getter = "getDurationScores")
    int[] durationScores;

    @Field(id = 7, getter = "getDurationWeight")
    int durationWeight;

    @Field(id = 8, getter = "getTransmissionRiskScores")
    int[] transmissionRiskScores;

    @Field(id = 9, getter = "getTransmissionRiskWeight")
    int transmissionRiskWeight;

    @Field(id = 10, getter = "getDurationAtAttenuationThresholds")
    int[] durationAtAttenuationThresholds;

    @Constructor
    ExposureConfiguration(
            @Param(id = 1) int minimumRiskScore,
            @Param(id = 2) int[] attenuationScores,
            @Param(id = 3) int attenuationWeight,
            @Param(id = 4) int[] daysSinceLastExposureScores,
            @Param(id = 5) int daysSinceLastExposureWeight,
            @Param(id = 6) int[] durationScores,
            @Param(id = 7) int durationWeight,
            @Param(id = 8) int[] transmissionRiskScores,
            @Param(id = 9) int transmissionRiskWeight,
            @Param(id = 10) int[] durationAtAttenuationThresholds) {
        this.minimumRiskScore = minimumRiskScore;
        this.attenuationScores = attenuationScores;
        this.attenuationWeight = attenuationWeight;
        this.daysSinceLastExposureScores = daysSinceLastExposureScores;
        this.daysSinceLastExposureWeight = daysSinceLastExposureWeight;
        this.durationScores = durationScores;
        this.durationWeight = durationWeight;
        this.transmissionRiskScores = transmissionRiskScores;
        this.transmissionRiskWeight = transmissionRiskWeight;
        this.durationAtAttenuationThresholds = durationAtAttenuationThresholds;
    }

    /**
     * Minimum risk score. Excludes exposure incidents with scores lower than this from calculation of
     * {@link ExposureSummary#getMaximumRiskScore}, {@link ExposureSummary#getSummationRiskScore}, and
     * {@link ExposureInformation#getTotalRiskScore}. Other returned fields are unaffected by this
     * setting.
     *
     * <p>Defaults to no minimum.
     */
    public int getMinimumRiskScore() {
        return minimumRiskScore;
    }

    /**
     * Scores for attenuation buckets. Must contain 8 scores, one for each bucket as defined below:
     *
     * <p><code>{@code
     * attenuationScores[0] when Attenuation > 73
     * attenuationScores[1] when 73 >= Attenuation > 63
     * attenuationScores[2] when 63 >= Attenuation > 51
     * attenuationScores[3] when 51 >= Attenuation > 33
     * attenuationScores[4] when 33 >= Attenuation > 27
     * attenuationScores[5] when 27 >= Attenuation > 15
     * attenuationScores[6] when 15 >= Attenuation > 10
     * attenuationScores[7] when 10 >= Attenuation
     * }</code>
     */
    public int[] getAttenuationScores() {
        return Arrays.copyOf(attenuationScores, attenuationScores.length);
    }

    /**
     * Weight to apply to the attenuation score. Must be in the range 0-100.
     *
     * <p>Reserved for future use.
     */
    public int getAttenuationWeight() {
        return attenuationWeight;
    }

    /**
     * Scores for days since last exposure buckets. Must contain 8 scores, one for each bucket as
     * defined below:
     *
     * <p><code>{@code
     * daysSinceLastExposureScores[0] when Days >= 14
     * daysSinceLastExposureScores[1] when Days >= 12
     * daysSinceLastExposureScores[2] when Days >= 10
     * daysSinceLastExposureScores[3] when Days >= 8
     * daysSinceLastExposureScores[4] when Days >= 6
     * daysSinceLastExposureScores[5] when Days >= 4
     * daysSinceLastExposureScores[6] when Days >= 2
     * daysSinceLastExposureScores[7] when Days >= 0
     * }</code>
     */
    public int[] getDaysSinceLastExposureScores() {
        return Arrays.copyOf(daysSinceLastExposureScores, daysSinceLastExposureScores.length);
    }

    /**
     * Weight to apply to the days since last exposure score. Must be in the range 0-100.
     *
     * <p>Reserved for future use.
     */
    public int getDaysSinceLastExposureWeight() {
        return daysSinceLastExposureWeight;
    }

    /**
     * Scores for duration buckets. Must contain 8 scores, one for each bucket as defined below:
     *
     * <p><code>{@code
     * durationScores[0] when Duration == 0
     * durationScores[1] when Duration <= 5
     * durationScores[2] when Duration <= 10
     * durationScores[3] when Duration <= 15
     * durationScores[4] when Duration <= 20
     * durationScores[5] when Duration <= 25
     * durationScores[6] when Duration <= 30
     * durationScores[7] when Duration  > 30
     * }</code>
     */
    public int[] getDurationScores() {
        return Arrays.copyOf(durationScores, durationScores.length);
    }

    /**
     * Weight to apply to the duration score. Must be in the range 0-100.
     *
     * <p>Reserved for future use.
     */
    public int getDurationWeight() {
        return durationWeight;
    }

    /**
     * Scores for transmission risk buckets. Must contain 8 scores, one for each bucket as defined
     * below:
     *
     * <p><code>{@code
     * transmissionRiskScores[0] when RISK_SCORE_LOWEST
     * transmissionRiskScores[1] when RISK_SCORE_LOW
     * transmissionRiskScores[2] when RISK_SCORE_LOW_MEDIUM
     * transmissionRiskScores[3] when RISK_SCORE_MEDIUM
     * transmissionRiskScores[4] when RISK_SCORE_MEDIUM_HIGH
     * transmissionRiskScores[5] when RISK_SCORE_HIGH
     * transmissionRiskScores[6] when RISK_SCORE_VERY_HIGH
     * transmissionRiskScores[7] when RISK_SCORE_HIGHEST
     * }</code>
     */
    public int[] getTransmissionRiskScores() {
        return Arrays.copyOf(transmissionRiskScores, transmissionRiskScores.length);
    }

    /**
     * Weight to apply to the transmission risk score. Must be in the range 0-100.
     *
     * <p>Reserved for future use.
     */
    public int getTransmissionRiskWeight() {
        return transmissionRiskWeight;
    }

    /**
     * Attenuation thresholds to apply when calculating duration at attenuation. Must contain two
     * thresholds, each in range of 0 - 255. durationAtAttenuationThresholds[0] has to be <=
     * durationAtAttenuationThresholds[1]. These are used used to populate {@link
     * ExposureSummary#getAttenuationDurationsInMinutes} and {@link
     * ExposureInformation#getAttenuationDurationsInMinutes}.
     */
    public int[] getDurationAtAttenuationThresholds() {
        return Arrays.copyOf(durationAtAttenuationThresholds, durationAtAttenuationThresholds.length);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof ExposureConfiguration) {
            ExposureConfiguration that = (ExposureConfiguration) obj;
            return Objects.equal(minimumRiskScore, that.getMinimumRiskScore())
                    && Arrays.equals(attenuationScores, that.getAttenuationScores())
                    && Objects.equal(attenuationWeight, that.getAttenuationWeight())
                    && Arrays.equals(daysSinceLastExposureScores, that.getDaysSinceLastExposureScores())
                    && Objects.equal(daysSinceLastExposureWeight, that.getDaysSinceLastExposureWeight())
                    && Arrays.equals(durationScores, that.getDurationScores())
                    && Objects.equal(durationWeight, that.getDurationWeight())
                    && Arrays.equals(transmissionRiskScores, that.getTransmissionRiskScores())
                    && Objects.equal(transmissionRiskWeight, that.getTransmissionRiskWeight())
                    && Arrays.equals(
                    durationAtAttenuationThresholds, that.getDurationAtAttenuationThresholds());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(
                minimumRiskScore,
                attenuationScores,
                attenuationWeight,
                daysSinceLastExposureScores,
                daysSinceLastExposureWeight,
                durationScores,
                durationWeight,
                transmissionRiskScores,
                transmissionRiskWeight,
                durationAtAttenuationThresholds);
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "ExposureConfiguration<"
                        + "minimumRiskScore: %d, "
                        + "attenuationScores: %s, "
                        + "attenuationWeight: %d, "
                        + "daysSinceLastExposureScores: %s, "
                        + "daysSinceLastExposureWeight: %d, "
                        + "durationScores: %s, "
                        + "durationWeight: %d, "
                        + "transmissionRiskScores: %s, "
                        + "transmissionRiskWeight: %d, "
                        + "durationAtAttenuationThresholds: %s"
                        + ">",
                minimumRiskScore,
                Arrays.toString(attenuationScores),
                attenuationWeight,
                Arrays.toString(daysSinceLastExposureScores),
                daysSinceLastExposureWeight,
                Arrays.toString(durationScores),
                durationWeight,
                Arrays.toString(transmissionRiskScores),
                transmissionRiskWeight,
                Arrays.toString(durationAtAttenuationThresholds));
    }

    /** A builder for {@link ExposureConfiguration}. */
    public static final class ExposureConfigurationBuilder {

        private int minimumRiskScore = 4;
        private int[] attenuationScores = new int[] {4, 4, 4, 4, 4, 4, 4, 4};
        private int attenuationWeight = 50;
        private int[] daysSinceLastExposureScores = new int[] {4, 4, 4, 4, 4, 4, 4, 4};
        private int daysSinceLastExposureWeight = 50;
        private int[] durationScores = new int[] {4, 4, 4, 4, 4, 4, 4, 4};
        private int durationWeight = 50;
        private int[] transmissionRiskScores = new int[] {4, 4, 4, 4, 4, 4, 4, 4};
        private int transmissionRiskWeight = 50;
        private int[] durationAtAttenuationThresholds = new int[] {50, 74};

        public ExposureConfigurationBuilder setMinimumRiskScore(int minimumRiskScore) {
            checkArgument(
                    minimumRiskScore >= 1 && minimumRiskScore <= 4096,
                    "minimumRiskScore (%s) must be >= 1 and <= 4096",
                    minimumRiskScore);
            this.minimumRiskScore = minimumRiskScore;
            return this;
        }

        public ExposureConfigurationBuilder setAttenuationScores(int... attenuationScores) {
            checkArgument(
                    attenuationScores.length == 8,
                    "attenuationScores (%s) must have exactly 8 elements",
                    Arrays.toString(attenuationScores));
            for (int attenuationScore : attenuationScores) {
                checkArgument(
                        attenuationScore >= 0 && attenuationScore <= 8,
                        "attenuationScore (%s) must be >= 0 and <= 8",
                        attenuationScore);
            }
            this.attenuationScores = Arrays.copyOf(attenuationScores, attenuationScores.length);
            return this;
        }

        public ExposureConfigurationBuilder setAttenuationWeight(int attenuationWeight) {
            checkArgument(
                    attenuationWeight >= 0 && attenuationWeight <= 100,
                    "attenuationWeight (%s) must be >= 0 and <= 100",
                    attenuationWeight);
            this.attenuationWeight = attenuationWeight;
            return this;
        }

        public ExposureConfigurationBuilder setDaysSinceLastExposureScores(
                int... daysSinceLastExposureScores) {
            checkArgument(
                    daysSinceLastExposureScores.length == 8,
                    "daysSinceLastExposureScores (%s) must have exactly 8 elements",
                    Arrays.toString(daysSinceLastExposureScores));
            for (int daysSinceLastExposureScore : daysSinceLastExposureScores) {
                checkArgument(
                        daysSinceLastExposureScore >= 0 && daysSinceLastExposureScore <= 8,
                        "daysSinceLastExposureScore (%s) must be >= 0 and <= 8",
                        daysSinceLastExposureScore);
            }
            this.daysSinceLastExposureScores =
                    Arrays.copyOf(daysSinceLastExposureScores, daysSinceLastExposureScores.length);
            return this;
        }

        public ExposureConfigurationBuilder setDaysSinceLastExposureWeight(
                int daysSinceLastExposureWeight) {
            checkArgument(
                    daysSinceLastExposureWeight >= 0 && daysSinceLastExposureWeight <= 100,
                    "daysSinceLastExposureWeight (%s) must be >= 0 and <= 100",
                    daysSinceLastExposureWeight);
            this.daysSinceLastExposureWeight = daysSinceLastExposureWeight;
            return this;
        }

        public ExposureConfigurationBuilder setDurationScores(int... durationScores) {
            checkArgument(
                    durationScores.length == 8,
                    "durationScores (%s) must have exactly 8 elements",
                    Arrays.toString(durationScores));
            for (int durationScore : durationScores) {
                checkArgument(
                        durationScore >= 0 && durationScore <= 8,
                        "durationScore (%s) must be >= 0 and <= 8",
                        durationScore);
            }
            this.durationScores = Arrays.copyOf(durationScores, durationScores.length);
            return this;
        }

        public ExposureConfigurationBuilder setDurationWeight(int durationWeight) {
            checkArgument(
                    durationWeight >= 0 && durationWeight <= 100,
                    "durationWeight (%s) must be >= 0 and <= 100",
                    durationWeight);
            this.durationWeight = durationWeight;
            return this;
        }

        public ExposureConfigurationBuilder setTransmissionRiskScores(int... transmissionRiskScores) {
            checkArgument(
                    transmissionRiskScores.length == 8,
                    "transmissionRiskScores (%s) must have exactly 8 elements",
                    Arrays.toString(transmissionRiskScores));
            for (int transmissionRiskScore : transmissionRiskScores) {
                checkArgument(
                        transmissionRiskScore >= 0 && transmissionRiskScore <= 8,
                        "transmissionRiskScore (%s) must be >= 0 and <= 8",
                        transmissionRiskScore);
            }
            this.transmissionRiskScores =
                    Arrays.copyOf(transmissionRiskScores, transmissionRiskScores.length);
            return this;
        }

        public ExposureConfigurationBuilder setTransmissionRiskWeight(int transmissionRiskWeight) {
            checkArgument(
                    transmissionRiskWeight >= 0 && transmissionRiskWeight <= 100,
                    "transmissionRiskWeight (%s) must be >= 0 and <= 100",
                    transmissionRiskWeight);
            this.transmissionRiskWeight = transmissionRiskWeight;
            return this;
        }

        public ExposureConfigurationBuilder setDurationAtAttenuationThresholds(
                int... durationAtAttenuationThresholds) {
            checkArgument(
                    durationAtAttenuationThresholds.length == 2,
                    "durationAtAttenuationThresholds (%s) must have exactly 2 elements",
                    Arrays.toString(durationAtAttenuationThresholds));
            for (int durationAtAttenuationThreshold : durationAtAttenuationThresholds) {
                checkArgument(
                        durationAtAttenuationThreshold >= 0 && durationAtAttenuationThreshold <= 255,
                        "durationAtAttenuationThreshold (%s) must be >= 0 and <= 255",
                        durationAtAttenuationThreshold);
            }
            checkArgument(
                    durationAtAttenuationThresholds[0] <= durationAtAttenuationThresholds[1],
                    "durationAtAttenuationThresholds[0] (%s) must be <= than"
                            + " durationAtAttenuationThresholds[1] (%s)",
                    durationAtAttenuationThresholds[0],
                    durationAtAttenuationThresholds[1]);
            this.durationAtAttenuationThresholds = durationAtAttenuationThresholds;
            return this;
        }

        public ExposureConfiguration build() {
            return new ExposureConfiguration(
                    minimumRiskScore,
                    attenuationScores,
                    attenuationWeight,
                    daysSinceLastExposureScores,
                    daysSinceLastExposureWeight,
                    durationScores,
                    durationWeight,
                    transmissionRiskScores,
                    transmissionRiskWeight,
                    durationAtAttenuationThresholds);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
import com.google.samples.exposurenotification.safeparcel.SafeParcelable;

import java.util.Arrays;
import java.util.Date;
import java.util.Locale;

import static com.google.common.base.Preconditions.checkArgument;


/**
 * Information about an exposure, meaning a single diagnosis key over a contiguous period of time
 * specified by durationMinutes.
 *
 * <p>The client can get the exposure information via {@link
 * ExposureNotificationClient#getExposureInformation}.
 *
 * @deprecated no longer used with Exposure Window API.
 */
@SafeParcelable.Class(creator = "ExposureInformationCreator")
@Deprecated
public final class ExposureInformation extends AbstractSafeParcelable {

    @Field(id = 1, getter = "getDateMillisSinceEpoch")
    long dateMillisSinceEpoch;

    @Field(id = 2, getter = "getDurationMinutes")
    int durationMinutes;

    @Field(id = 3, getter = "getAttenuationValue")
    int attenuationValue;

    @Field(id = 4, getter = "getTransmissionRiskLevel")
    @RiskLevel
    int transmissionRiskLevel;

    @Field(id = 5, getter = "getTotalRiskScore")
    int totalRiskScore;

    @Field(id = 6, getter = "getAttenuationDurationsInMinutes")
    int[] attenuationDurations;

    @Constructor
    ExposureInformation(
            @Param(id = 1) long dateMillisSinceEpoch,
            @Param(id = 2) int durationMinutes,
            @Param(id = 3) int attenuationValue,
            @Param(id = 4) int transmissionRiskLevel,
            @Param(id = 5) int totalRiskScore,
            @Param(id = 6) int[] attenuationDurations) {
        this.dateMillisSinceEpoch = dateMillisSinceEpoch;
        this.durationMinutes = durationMinutes;
        this.attenuationValue = attenuationValue;
        this.transmissionRiskLevel = transmissionRiskLevel;
        this.totalRiskScore = totalRiskScore;
        this.attenuationDurations = attenuationDurations;
    }

    /** Day-level resolution that the exposure occurred, in milliseconds since epoch. */
    public long getDateMillisSinceEpoch() {
        return dateMillisSinceEpoch;
    }

    /** Length of exposure in 5 minute increments, with a 30 minute maximum. */
    public int getDurationMinutes() {
        return durationMinutes;
    }

    /**
     * The time-weighted signal strength attenuation value which goes into {@link #getTotalRiskScore}.
     * Value will be between 0 and 255.
     */
    public int getAttenuationValue() {
        return attenuationValue;
    }

    /** The transmission risk associated with the matched diagnosis key. */
    @RiskLevel
    public int getTransmissionRiskLevel() {
        return transmissionRiskLevel;
    }

    /**
     * The total risk calculated for the exposure. See {@link ExposureConfiguration} for more
     * information about what is represented by the risk score, it will be a value 0-4096.
     */
    public int getTotalRiskScore() {
        return totalRiskScore;
    }

    /**
     * Array of durations in minutes at certain radio signal attenuations. The thresholds are defined
     * in {@link ExposureConfiguration#getDurationAtAttenuationThresholds}.
     *
     * <p>Array index 0: Sum of durations for all exposures when attenuation < low threshold.
     *
     * <p>Array index 1: Sum of durations for all exposures when low threshold <= attenuation < high
     * threshold.
     *
     * <p>Array index 2: Sum of durations for all exposures when attenuation >= high threshold.
     */
    public int[] getAttenuationDurationsInMinutes() {
        return Arrays.copyOf(attenuationDurations, attenuationDurations.length);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof ExposureInformation) {
            ExposureInformation that = (ExposureInformation) obj;
            return Objects.equal(dateMillisSinceEpoch, that.getDateMillisSinceEpoch())
                    && Objects.equal(durationMinutes, that.getDurationMinutes())
                    && Objects.equal(attenuationValue, that.getAttenuationValue())
                    && Objects.equal(transmissionRiskLevel, that.getTransmissionRiskLevel())
                    && Objects.equal(totalRiskScore, that.getTotalRiskScore())
                    && Arrays.equals(attenuationDurations, that.getAttenuationDurationsInMinutes());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(
                dateMillisSinceEpoch,
                durationMinutes,
                attenuationValue,
                transmissionRiskLevel,
                totalRiskScore,
                attenuationDurations);
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "ExposureInformation<"
                        + "date: %s, "
                        + "dateMillisSinceEpoch: %d, "
                        + "durationMinutes: %d, "
                        + "attenuationValue: %d, "
                        + "transmissionRiskLevel: %d, "
                        + "totalRiskScore: %d, "
                        + "attenuationDurations: %s"
                        + ">",
                new Date(dateMillisSinceEpoch),
                dateMillisSinceEpoch,
                durationMinutes,
                attenuationValue,
                transmissionRiskLevel,
                totalRiskScore,
                Arrays.toString(attenuationDurations));
    }

    /** A builder for {@link ExposureInformation}. */
    public static final class ExposureInformationBuilder {

        private long dateMillisSinceEpoch = 0;
        private int durationMinutes = 0;
        private int attenuationValue = 0;
        @RiskLevel private int transmissionRiskLevel = RiskLevel.RISK_LEVEL_INVALID;
        private int totalRiskScore = 0;
        private int[] attenuationDurations = new int[] {0, 0};

        public ExposureInformationBuilder setDateMillisSinceEpoch(long dateMillisSinceEpoch) {
            // This check only allows dates >= 1970-01-01, which is fairly arbitrary. Maybe we want to
            // make these bounds a bit tighter?
            checkArgument(
                    dateMillisSinceEpoch >= 0,
                    "dateMillisSinceEpoch (%s) must be >= 0",
                    dateMillisSinceEpoch);
            this.dateMillisSinceEpoch = dateMillisSinceEpoch;
            return this;
        }

        public ExposureInformationBuilder setDurationMinutes(int durationMinutes) {
            checkArgument(
                    durationMinutes % 5 == 0,
                    "durationMinutes (%s) must be an increment of 5",
                    durationMinutes);
            checkArgument(durationMinutes <= 30, "durationMinutes (%s) must be <= 30", durationMinutes);
            this.durationMinutes = durationMinutes;
            return this;
        }

        public ExposureInformationBuilder setAttenuationValue(int attenuationValue) {
            checkArgument(
                    attenuationValue >= 0 && attenuationValue <= 255,
                    "attenuationValue (%s) must be >= 0 and <= 255",
                    attenuationValue);
            this.attenuationValue = attenuationValue;
            return this;
        }

        public ExposureInformationBuilder setTransmissionRiskLevel(
                @RiskLevel int transmissionRiskLevel) {
            checkArgument(
                    transmissionRiskLevel >= 0 && transmissionRiskLevel <= 8,
                    "transmissionRiskLevel (%s) must be >= 0 and <= 8",
                    transmissionRiskLevel);
            this.transmissionRiskLevel = transmissionRiskLevel;
            return this;
        }

        public ExposureInformationBuilder setTotalRiskScore(int totalRiskScore) {
            checkArgument(
                    totalRiskScore >= 0 && totalRiskScore <= 4096,
                    "totalRiskScore (%s) must be >= 0 and <= 4096",
                    totalRiskScore);
            this.totalRiskScore = totalRiskScore;
            return this;
        }

        public ExposureInformationBuilder setAttenuationDurations(int[] attenuationDurations) {
            for (int attenuationDuration : attenuationDurations) {
                checkArgument(
                        attenuationDuration >= 0, "attenuationDuration (%s) must be >= 0", attenuationDuration);
            }
            this.attenuationDurations = Arrays.copyOf(attenuationDurations, attenuationDurations.length);
            return this;
        }

        public ExposureInformation build() {
            return new ExposureInformation(
                    dateMillisSinceEpoch,
                    durationMinutes,
                    attenuationValue,
                    transmissionRiskLevel,
                    totalRiskScore,
                    attenuationDurations);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import com.google.android.play.core.tasks.Task;

import java.io.File;
import java.util.List;
import java.util.Set;

/** Interface for contact tracing APIs. */
public interface ExposureNotificationClient {

    /**
     * Action which will be invoked via a BroadcastReceiver as a callback when the user has an updated
     * exposure status. Also see {@link #EXTRA_EXPOSURE_SUMMARY} and {@link #EXTRA_TOKEN}, which will
     * be included in this broadcast.
     */
    String ACTION_EXPOSURE_STATE_UPDATED =
            "com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATED";

    /**
     * Action which will be invoked via a BroadcastReceiver as a callback when matching has finished
     * and no matches were found. Also see {@link #EXTRA_TOKEN}, which will be included in this
     * broadcast.
     */
    String ACTION_EXPOSURE_NOT_FOUND =
            "com.google.android.gms.exposurenotification.ACTION_EXPOSURE_NOT_FOUND";

    /**
     * Extra attached to the {@link #ACTION_EXPOSURE_STATE_UPDATED} broadcast, giving a summary of the
     * exposure details detected. Also see {@link #getExposureSummary}.
     *
     * @deprecated {@link ExposureSummary} is no longer provided when using the {@link
     *     #getExposureWindows} API. Instead, use {@link #getDailySummaries}.
     */
    @Deprecated
    String EXTRA_EXPOSURE_SUMMARY =
            "com.google.android.gms.exposurenotification.EXTRA_EXPOSURE_SUMMARY";

    /**
     * Extra attached to the {@link #ACTION_EXPOSURE_STATE_UPDATED} broadcast, providing the token
     * associated with the {@link #provideDiagnosisKeys} request.
     *
     * @deprecated Tokens are no longer used. Instead, prefer using the tokenless versions of {@link
     *     #provideDiagnosisKeys}, {@link #getExposureWindows}, and {@link #getDailySummaries}.
     */
    @Deprecated String EXTRA_TOKEN = "com.google.android.gms.exposurenotification.EXTRA_TOKEN";

    /** Activity action which shows the exposure notification settings screen. */
    String ACTION_EXPOSURE_NOTIFICATION_SETTINGS =
            "com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS";

    /**
     * Action which will be invoked via a BroadcastReceiver when the user modifies the state of
     * exposure notifications via the Google Settings page. {@link #EXTRA_SERVICE_STATE} will be
     * included as part of this broadcast.
     */
    String ACTION_SERVICE_STATE_UPDATED =
            "com.google.android.gms.exposurenotification.ACTION_SERVICE_STATE_UPDATED";

    /**
     * Boolean extra attached to the {@link #ACTION_SERVICE_STATE_UPDATED} broadcast signifying
     * whether the service is enabled or disabled.
     */
    String EXTRA_SERVICE_STATE = "com.google.android.gms.exposurenotification.EXTRA_SERVICE_STATE";

    /**
     * Token to be used with ExposureWindows API. Must be used with {@link #provideDiagnosisKeys}
     * request when later using {@link #getExposureWindows}.
     *
     * @deprecated Tokens are no longer used. Instead, prefer using the tokenless versions of {@link
     *     #provideDiagnosisKeys}, {@link #getExposureWindows}, and {@link #getDailySummaries}.
     */
    @Deprecated String TOKEN_A = "TYZWQ32170AXEUVCDW7A";

    /**
     * Settings.Global key for indicating whether a device supports locationless scanning for Exposure
     * Notifications.
     *
     * @hide
     */
    public static final String LOCATIONLESS_SCANNING_SUPPORTED_SETTING =
            "bluetooth_sanitized_exposure_notification_supported";

    /**
     * Starts BLE broadcasts and scanning based on the defined protocol.
     *
     * <p>If not previously started, this shows a user dialog for consent to start exposure detection
     * and get permission.
     *
     * <p>Callbacks regarding exposure status will be provided via a BroadcastReceiver. Clients should
     * register a receiver in their AndroidManifest which can handle the following action:
     *
     * <ul>
     *   <li><code>com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATED</code>
     * </ul>
     *
     * This receiver should also be guarded by the <code>
     * com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK</code> permission so that
     * other apps are not able to fake this broadcast.
     */
    Task<Void> start();

    /**
     * Starts BLE broadcasts and scanning for a given app.
     *
     * <p>Same as {@link #start()}, but could be called by a 1P client on behalf of the app. For
     * example, this could be useful for Play Store.
     *
     * @param packageName the package of the app about which the requester inquires
     * @param appName the human readable name of that app to be displayed in the resulted UI
     * @param signatureHash the signature of that app
     * @hide
     */
    Task<Void> startForPackage(String packageName, String appName, byte[] signatureHash);

    /**
     * Starts BLE broadcasts and scanning for a given app.
     *
     * <p>Same as {@link #start()}, but could be called by a 1P client on behalf of the app. For
     * example, this could be useful for Play Store.
     *
     * @param packageName the package of the app about which the requester inquires
     * @param appName the human readable name of that app to be displayed in the resulted UI
     * @param signatureHash the signature of that app
     * @param skipOptInDialog whether or not the opt in dialog should be skipped. This dialog should
     *     only be skipped in the case where the caller has displayed the appropriate approval screens
     *     to the user already and have gotten opt in from the user.
     * @hide
     */
    Task<Void> startForPackage(
            String packageName, String appName, byte[] signatureHash, boolean skipOptInDialog);

    /**
     * Starts BLE broadcasts and scanning for a given app.
     *
     * <p>Same as {@link #start()}, but could be called by a 1P client on behalf of the app. For
     * example, this could be useful for Play Store.
     *
     * @param packageName the package of the app about which the requester inquires
     * @param appName the human readable name of that app to be displayed in the resulted UI
     * @param signatureHash the signature of that app
     * @param skipOptInDialog whether or not the opt in dialog should be skipped. This dialog should
     *     only be skipped in the case where the caller has displayed the appropriate approval screens
     *     to the user already and have gotten opt in from the user.
     * @param configuration settings which may be passed onto the app after it has finished starting.
     * @hide
     */
    Task<Void> startForPackage(
            String packageName,
            String appName,
            byte[] signatureHash,
            boolean skipOptInDialog,
            PackageConfiguration configuration);

    /**
     * Disables advertising and scanning. Contents of the database and keys will remain.
     *
     * <p>If the client app has been uninstalled by the user, this will be automatically invoked and
     * the database and keys will be wiped from the device.
     */
    Task<Void> stop();

    /** Indicates whether contact tracing is currently running for the requesting app. */
    Task<Boolean> isEnabled();

    /**
     * Indicates whether contact tracing is currently running for a given app.
     *
     * <p>For use by 1P clients only.
     *
     * @param packageName the package of the app about which the requester inquires
     * @param signatureHash the signature of that app
     * @hide
     */
    Task<Boolean> isEnabledForPackage(String packageName, byte[] signatureHash);

    /**
     * Gets {@link TemporaryExposureKey} history to be stored on the server.
     *
     * <p>This should only be done after proper verification is performed on the client side that the
     * user is diagnosed positive. Each key returned will have an unknown transmission risk level,
     * clients should choose an appropriate risk level for these keys before uploading them to the
     * server.
     *
     * <p>The keys provided here will only be from previous days; keys will not be released until
     * after they are no longer an active exposure key.
     *
     * <p>This shows a user permission dialog for sharing and uploading data to the server.
     */
    Task<List<TemporaryExposureKey>> getTemporaryExposureKeyHistory();

    /**
     * Provides a list of diagnosis key files for exposure checking. The files are to be synced from
     * the server. Old diagnosis keys (for example older than 14 days), will be ignored.
     *
     * <p>Diagnosis keys will be stored and matching will be performed in the near future, after which
     * you’ll receive a broadcast with the {@link #ACTION_EXPOSURE_STATE_UPDATED} action. If no
     * matches are found, you'll receive an {@link #ACTION_EXPOSURE_NOT_FOUND} action.
     *
     * <p>The diagnosis key files must be signed appropriately. Exposure configuration options can be
     * provided to tune the matching algorithm. A unique token for this batch can also be provided,
     * which will be used to associate the matches with this request as part of {@link
     * #getExposureSummary} and {@link #getExposureInformation}. Alternatively, the same token can be
     * passed in multiple times to concatenate results.
     *
     * <p>After the result Task has returned, keyFiles can be deleted.
     *
     * <p>Results for a given token remain for 14 days.
     *
     * @deprecated Tokens and configuration are no longer used. Instead, prefer using the tokenless,
     *     configuration-less version of {@link #provideDiagnosisKeys}.
     */
    @Deprecated
    Task<Void> provideDiagnosisKeys(
            List<File> keyFiles, ExposureConfiguration configuration, String token);

    /**
     * Provides a list of diagnosis key files for exposure checking. The files are to be synced from
     * the server. Old diagnosis keys (for example older than 14 days), will be ignored.
     *
     * <p>Diagnosis keys will be stored and matching will be performed in the near future, after which
     * you’ll receive a broadcast with the {@link #ACTION_EXPOSURE_STATE_UPDATED} action. If no
     * matches are found, you'll receive an {@link #ACTION_EXPOSURE_NOT_FOUND} action.
     *
     * <p>The diagnosis key files must be signed appropriately. Results from this request can also be
     * queried at any time via {@link #getExposureWindows} and {@link #getDailySummaries}.
     *
     * <p>After the result Task has returned, keyFiles can be deleted.
     *
     * <p>Results remain for 14 days.
     */
    Task<Void> provideDiagnosisKeys(List<File> keyFiles);

    /**
     * Provides diagnosis key files for exposure checking. The files are to be synced from the server.
     * Old diagnosis keys (for example older than 14 days), will be ignored.
     *
     * <p>Diagnosis keys will be stored and matching will be performed in the near future, after which
     * you’ll receive a broadcast with the {@link #ACTION_EXPOSURE_STATE_UPDATED} action. If no
     * matches are found, you'll receive an {@link #ACTION_EXPOSURE_NOT_FOUND} action.
     *
     * <p>The diagnosis key files must be signed appropriately. Results from this request can also be
     * queried at any time via {@link #getExposureWindows} and {@link #getDailySummaries}.
     *
     * <p>After the result Task has returned, files can be deleted.
     *
     * <p>Results remain for 14 days.
     *
     * <p>This method is identical to providing a list of key files, but skips checking the EN version
     * and assumes the provider can be used.
     */
    Task<Void> provideDiagnosisKeys(DiagnosisKeyFileProvider provider);

    /**
     * Retrieves the list of exposure windows corresponding to the TEKs given to provideKeys with
     * token=TOKEN_A.
     *
     * <p>Long exposures to one TEK are split into windows of up to 30 minutes of scans, so a given
     * TEK may lead to several exposure windows if beacon sightings for it spanned more than 30
     * minutes. The link between them (the fact that they all correspond to the same TEK) is lost
     * because those windows are shuffled before being returned and the underlying TEKs are not
     * exposed by the API.
     *
     * <p>The provided token must be TOKEN_A.
     *
     * @deprecated Tokens are no longer used. Instead, prefer using the tokenless version of {@link
     *     #getExposureWindows}.
     */
    @Deprecated
    Task<List<ExposureWindow>> getExposureWindows(String token);

    /**
     * Retrieves the list of exposure windows corresponding to the TEKs given to {@link
     * #provideDiagnosisKeys}.
     *
     * <p>Long exposures to one TEK are split into windows of up to 30 minutes of scans, so a given
     * TEK may lead to several exposure windows if beacon sightings for it spanned more than 30
     * minutes. The link between them (the fact that they all correspond to the same TEK) is lost
     * because those windows are shuffled before being returned and the underlying TEKs are not
     * exposed by the API.
     */
    Task<List<ExposureWindow>> getExposureWindows();

    /**
     * Gets a summary of the exposure calculation for the token, which should match the token provided
     * in {@link #provideDiagnosisKeys}.
     *
     * @deprecated When using the {@link ExposureWindow} API, use {@link #getDailySummaries} instead.
     */
    @Deprecated
    Task<ExposureSummary> getExposureSummary(String token);

    /**
     * Gets detailed information about exposures that have occurred related to the provided token,
     * which should match the token provided in {@link #provideDiagnosisKeys}.
     *
     * <p>When multiple {@link ExposureInformation} objects are returned, they can be:
     *
     * <ul>
     *   <li>Multiple encounters with a single diagnosis key.
     *   <li>Multiple encounters with the same device across key rotation boundaries.
     *   <li>Encounters with multiple devices.
     * </ul>
     *
     * Records of calls to this method will be retained and viewable by the user.
     *
     * @deprecated When using the {@link ExposureWindow} API, use {@link #getExposureWindows} instead.
     */
    @Deprecated
    Task<List<ExposureInformation>> getExposureInformation(String token);

    /** Gets the current Exposure Notification version. */
    Task<Long> getVersion();

    /** Gets {@link CalibrationConfidence} of the current device. */
    Task<Integer> getCalibrationConfidence();

    /**
     * Retrieves the per-day exposure summaries associated with the provided configuration.
     *
     * <p>A valid configuration must be provided to compute the summaries.
     */
    Task<List<DailySummary>> getDailySummaries(DailySummariesConfig dailySummariesConfig);

    /**
     * Sets the diagnosis keys data mapping if it wasn't already changed recently.
     *
     * <p>If called twice within 7 days, the second call will have no effect and will raise an
     * exception with status code FAILED_RATE_LIMITED.
     */
    Task<Void> setDiagnosisKeysDataMapping(DiagnosisKeysDataMapping diagnosisKeysMetadataMapping);

    /** Retrieves the current {@link DiagnosisKeysDataMapping}. */
    Task<DiagnosisKeysDataMapping> getDiagnosisKeysDataMapping();

    /**
     * Checks whether the device supports Exposure Notification BLE scanning without requiring
     * location to be enabled first.
     */
    boolean deviceSupportsLocationlessScanning();

    /** Gets the current Exposure Notification status. */
    Task<Set<ExposureNotificationStatus>> getStatus();

    /**
     * Retrieves the associated {@link PackageConfiguration} for the calling package. Note that this
     * value can be null if no configuration was when starting.
     */
    Task<PackageConfiguration> getPackageConfiguration();
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import com.google.common.collect.ImmutableSet;
import java.util.Set;

/** Detail status for exposure notification service. */
public enum ExposureNotificationStatus {
    /** Exposure notification is running. */
    ACTIVATED(0x00000001),
    /** Exposure notification is not running. */
    INACTIVATED(0x00000002),
    /** Bluetooth is not enabled. */
    BLUETOOTH_DISABLED(0x00000004),
    /** Location is not enabled. */
    LOCATION_DISABLED(0x00000008),
    /** User is not consent for the client. */
    NO_CONSENT(0x00000010),
    /** The client is not in approved client list. */
    NOT_IN_WHITELIST(0x00000020),
    /** Can't detected the BLE supporting of this device due to bluetooth is not enabled. */
    BLUETOOTH_SUPPORT_UNKNOWN(0x00000040),
    /** Hardware of this device doesn't support exposure notification. */
    HW_NOT_SUPPORT(0x00000080),
    /** There is another client running as active client. */
    FOCUS_LOST(0x00000100),
    /** Device storage is not sufficient for exposure notification. */
    LOW_STORAGE(0x00000200),
    /** Current status is unknown. */
    UNKNOWN(0x00000400),
    /** Exposure notification is not supported. */
    EN_NOT_SUPPORT(0x00000800),
    /** Exposure notification is not supported for current user profile. */
    USER_PROFILE_NOT_SUPPORT(0x00001000);

    private final long value;

    ExposureNotificationStatus(long value) {
        this.value = value;
    }

    public static long getValues(Set<ExposureNotificationStatus> statusSet) {
        long result = 0;
        for (ExposureNotificationStatus status : statusSet) {
            result |= status.value;
        }
        return result;
    }

    public static ImmutableSet<ExposureNotificationStatus> fromValue(long value) {
        ImmutableSet.Builder<ExposureNotificationStatus> builder = ImmutableSet.builder();
        for (ExposureNotificationStatus status : ExposureNotificationStatus.values()) {
            if ((status.value & value) != 0) {
                builder.add(status);
            }
        }
        return builder.build();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

/**
 * Status codes for nearby contact tracing results.
 */
public final class ExposureNotificationStatusCodes {

    private ExposureNotificationStatusCodes() {
    }

    // IMPORTANT NOTE: The codes referenced in this file are used on both the client and service
    // side, and must not be modified after launch. It is fine to add new codes, but previously
    // existing codes must be left unmodified.

    // Common status codes that may be used by a variety of actions.

    /**
     * The operation was successful.
     */
    public static final int SUCCESS = 0;

    /**
     * An internal error occurred. Retrying should resolve the problem.
     */
    public static final int INTERNAL_ERROR = 8;

    /**
     * The application is misconfigured. This error is not recoverable and will be treated as fatal.
     * The developer should look at the logs after this to determine more actionable information.
     */
    public static final int DEVELOPER_ERROR = 10;

    /**
     * The operation failed, without any more information.
     */
    public static final int FAILED = 13;

    /**
     * A blocking call was interrupted while waiting and did not run to completion.
     */
    public static final int INTERRUPTED = 14;

    /**
     * Timed out while awaiting the result.
     */
    public static final int TIMEOUT = 15;

    /**
     * The result was canceled.
     */
    public static final int CANCELED = 16;

    /**
     * The app was already in the requested state so the call did nothing.
     */
    public static final int FAILED_ALREADY_STARTED = 39500;

    /**
     * The hardware capability of the device was not supported.
     */
    public static final int FAILED_NOT_SUPPORTED = 39501;

    /**
     * The user rejected the opt-in state.
     */
    public static final int FAILED_REJECTED_OPT_IN = 39502;

    /**
     * The functionality was disabled by the user or the phone.
     */
    public static final int FAILED_SERVICE_DISABLED = 39503;

    /**
     * The bluetooth was powered off.
     */
    public static final int FAILED_BLUETOOTH_DISABLED = 39504;

    /**
     * The service was disabled for some reasons temporarily.
     */
    public static final int FAILED_TEMPORARILY_DISABLED = 39505;

    /**
     * The operation failed during a disk read/write.
     */
    public static final int FAILED_DISK_IO = 39506;

    /**
     * The client is unauthorized to access the APIs.
     */
    public static final int FAILED_UNAUTHORIZED = 39507;

    /**
     * The client has been rate limited for access to this API.
     */
    public static final int FAILED_RATE_LIMITED = 39508;

    /**
     * Returns an untranslated debug (not user-friendly!) string based on the current status code.
     */
    public static String getStatusCodeString(int statusCode) {
        switch (statusCode) {
            case FAILED_ALREADY_STARTED:
                return "FAILED_ALREADY_STARTED";
            case FAILED_NOT_SUPPORTED:
                return "FAILED_NOT_SUPPORTED";
            case FAILED_REJECTED_OPT_IN:
                return "FAILED_REJECTED_OPT_IN";
            case FAILED_SERVICE_DISABLED:
                return "FAILED_SERVICE_DISABLED";
            case FAILED_BLUETOOTH_DISABLED:
                return "FAILED_BLUETOOTH_DISABLED";
            case FAILED_TEMPORARILY_DISABLED:
                return "FAILED_TEMPORARILY_DISABLED";
            case FAILED_DISK_IO:
                return "FAILED_DISK_IO";
            case FAILED_UNAUTHORIZED:
                return "FAILED_UNAUTHORIZED";
            case FAILED_RATE_LIMITED:
                return "FAILED_RATE_LIMITED";
            default:
                return "unknown status code: " + statusCode;
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
import com.google.samples.exposurenotification.safeparcel.ReflectedParcelable;
import com.google.samples.exposurenotification.safeparcel.SafeParcelable;

import java.util.Arrays;
import java.util.Locale;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * Summary information about recent exposures.
 *
 * <p>The client can get this information via {@link ExposureNotificationClient#getExposureSummary}.
 *
 * @deprecated no longer used with Exposure Window API.
 */
@SafeParcelable.Class(creator = "ExposureSummaryCreator")
@Deprecated
public final class ExposureSummary extends AbstractSafeParcelable implements ReflectedParcelable {

    @Field(id = 1, getter = "getDaysSinceLastExposure")
    int daysSinceLastExposure;

    @Field(id = 2, getter = "getMatchedKeyCount")
    int matchedKeyCount;

    @Field(id = 3, getter = "getMaximumRiskScore")
    int maximumRiskScore;

    @Field(id = 4, getter = "getAttenuationDurationsInMinutes")
    int[] attenuationDurations;

    @Field(id = 5, getter = "getSummationRiskScore")
    int summationRiskScore;

    @Constructor
    ExposureSummary(
            @Param(id = 1) int daysSinceLastExposure,
            @Param(id = 2) int matchedKeyCount,
            @Param(id = 3) int maximumRiskScore,
            @Param(id = 4) int[] attenuationDurations,
            @Param(id = 5) int summationRiskScore) {
        this.daysSinceLastExposure = daysSinceLastExposure;
        this.matchedKeyCount = matchedKeyCount;
        this.maximumRiskScore = maximumRiskScore;
        this.attenuationDurations = attenuationDurations;
        this.summationRiskScore = summationRiskScore;
    }

    /**
     * Days since last match to a diagnosis key from the server. 0 is today, 1 is yesterday, etc. Only
     * valid if {@link #getMatchedKeyCount} > 0.
     */
    public int getDaysSinceLastExposure() {
        return daysSinceLastExposure;
    }

    /** Number of matched diagnosis keys. */
    public int getMatchedKeyCount() {
        return matchedKeyCount;
    }

    /** The highest risk score of all exposure incidents, it will be a value 0-4096. */
    public int getMaximumRiskScore() {
        return maximumRiskScore;
    }

    /**
     * Array of durations in minutes at certain radio signal attenuations. The thresholds are defined
     * in {@link ExposureConfiguration#getDurationAtAttenuationThresholds}.
     *
     * <p>Array index 0: Sum of durations for all exposures when attenuation < low threshold.
     *
     * <p>Array index 1: Sum of durations for all exposures when low threshold <= attenuation < high
     * threshold.
     *
     * <p>Array index 2: Sum of durations for all exposures when attenuation >= high threshold.
     *
     * <p>These durations are aggregated across all exposures and capped at 30 minutes.
     */
    public int[] getAttenuationDurationsInMinutes() {
        return Arrays.copyOf(attenuationDurations, attenuationDurations.length);
    }

    /** The summation of risk scores of all exposure incidents. */
    public int getSummationRiskScore() {
        return summationRiskScore;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof ExposureSummary) {
            ExposureSummary that = (ExposureSummary) obj;
            return Objects.equal(daysSinceLastExposure, that.getDaysSinceLastExposure())
                    && Objects.equal(matchedKeyCount, that.getMatchedKeyCount())
                    && Objects.equal(maximumRiskScore, that.getMaximumRiskScore())
                    && Arrays.equals(attenuationDurations, that.getAttenuationDurationsInMinutes())
                    && Objects.equal(summationRiskScore, that.getSummationRiskScore());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(
                daysSinceLastExposure,
                matchedKeyCount,
                maximumRiskScore,
                attenuationDurations,
                summationRiskScore);
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "ExposureSummary<"
                        + "daysSinceLastExposure: %d, "
                        + "matchedKeyCount: %d, "
                        + "maximumRiskScore: %d, "
                        + "attenuationDurations: %s, "
                        + "summationRiskScore: %d"
                        + ">",
                daysSinceLastExposure,
                matchedKeyCount,
                maximumRiskScore,
                Arrays.toString(attenuationDurations),
                summationRiskScore);
    }

    /** A builder for {@link ExposureSummary}. */
    public static final class ExposureSummaryBuilder {

        private int daysSinceLastExposure = 0;
        private int matchedKeyCount = 0;
        private int maximumRiskScore = 0;
        private int[] attenuationDurations = new int[] {0, 0, 0};
        private int summationRiskScore = 0;

        public ExposureSummaryBuilder setDaysSinceLastExposure(int daysSinceLastExposure) {
            checkArgument(
                    daysSinceLastExposure >= 0,
                    "daysSinceLastExposure (%s) must be >= 0",
                    daysSinceLastExposure);
            this.daysSinceLastExposure = daysSinceLastExposure;
            return this;
        }

        public ExposureSummaryBuilder setMatchedKeyCount(int matchedKeyCount) {
            checkArgument(matchedKeyCount >= 0, "matchedKeyCount (%s) must be >= 0", matchedKeyCount);
            this.matchedKeyCount = matchedKeyCount;
            return this;
        }

        public ExposureSummaryBuilder setMaximumRiskScore(int maximumRiskScore) {
            checkArgument(
                    maximumRiskScore >= 0 && maximumRiskScore <= 4096,
                    "maximumRiskScore (%s) must be >= 0 and <= 4096",
                    maximumRiskScore);
            this.maximumRiskScore = maximumRiskScore;
            return this;
        }

        public ExposureSummaryBuilder setAttenuationDurations(int[] attenuationDurations) {
            checkArgument(attenuationDurations.length == 3);
            for (int attenuationDuration : attenuationDurations) {
                checkArgument(
                        attenuationDuration >= 0, "attenuationDuration (%s) must be >= 0", attenuationDuration);
            }
            this.attenuationDurations = Arrays.copyOf(attenuationDurations, attenuationDurations.length);
            return this;
        }

        public ExposureSummaryBuilder setSummationRiskScore(int summationRiskScore) {
            checkArgument(
                    summationRiskScore >= 0, "summationRiskScore (%s) must be >= 0", summationRiskScore);
            this.summationRiskScore = summationRiskScore;
            return this;
        }

        public ExposureSummary build() {
            return new ExposureSummary(
                    daysSinceLastExposure,
                    matchedKeyCount,
                    maximumRiskScore,
                    attenuationDurations,
                    summationRiskScore);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.samples.exposurenotification.ExposureNotificationEnums;

import java.util.List;
import java.util.Locale;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * A duration of up to 30 minutes during which beacons from a TEK were observed.
 *
 * <p>Each ExposureWindow corresponds to a single TEK, but one TEK can lead to several
 * ExposureWindow due to random 15-30 minutes cuts. See getExposureWindows() for more info.
 *
 * <p>The TEK itself isn't exposed by the API.
 */
public final class ExposureWindow {

    long dateMillisSinceEpoch;

    final List<ScanInstance> scanInstances;
    @ReportType
    final int reportType;

    @Infectiousness
    final int infectiousness;

    @CalibrationConfidence
    final int calibrationConfidence;

    ExposureWindow(
            long dateMillisSinceEpoch,
            List<ScanInstance> scanInstances,
            @ReportType int reportType,
            @Infectiousness int infectiousness,
            @CalibrationConfidence int calibrationConfidence) {
        this.dateMillisSinceEpoch = dateMillisSinceEpoch;
        this.scanInstances = ImmutableList.copyOf(scanInstances);
        this.reportType = reportType;
        this.infectiousness = infectiousness;
        this.calibrationConfidence = calibrationConfidence;
    }

    /**
     * Returns the epoch time in milliseconds the exposure occurred. This will represent the start of
     * a day in UTC.
     */
    public long getDateMillisSinceEpoch() {
        return dateMillisSinceEpoch;
    }

    /**
     * Sightings of this ExposureWindow, time-ordered.
     *
     * <p>Each sighting corresponds to a scan (of a few seconds) during which a beacon with the TEK
     * causing this exposure was observed.
     */
    public List<ScanInstance> getScanInstances() {
        return scanInstances;
    }

    /**
     * Report Type of the TEK that caused this exposure
     *
     * <p>TEKs with no report type set are returned with reportType=CONFIRMED_TEST.
     *
     * <p>TEKs with RECURSIVE report type may be dropped because this report type is reserved for
     * future use.
     *
     * <p>TEKs with REVOKED or invalid report types do not lead to exposures.
     */
    @ReportType
    public int getReportType() {
        return reportType;
    }

    /**
     * Infectiousness of the TEK that caused this exposure, computed from the days since onset of
     * symptoms using the daysToInfectiousnessMapping.
     */
    @Infectiousness
    public int getInfectiousness() {
        return infectiousness;
    }

    /**
     * Confidence of the BLE Transmit power calibration of the transmitting device.
     */
    @CalibrationConfidence
    public int getCalibrationConfidence() {
        return calibrationConfidence;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ExposureWindow that = (ExposureWindow) o;
        return reportType == that.reportType
                && dateMillisSinceEpoch == that.dateMillisSinceEpoch
                && scanInstances.equals(that.scanInstances)
                && infectiousness == that.infectiousness
                && calibrationConfidence == that.calibrationConfidence;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(
                dateMillisSinceEpoch, scanInstances, reportType, infectiousness, calibrationConfidence);
    }

    @Override
    public String toString() {
        return "ExposureWindow{"
                + "dateMillisSinceEpoch="
                + dateMillisSinceEpoch
                + ", reportType="
                + reportType
                + ", scanInstances="
                + scanInstances
                + ", infectiousness="
                + infectiousness
                + ", calibrationConfidence="
                + calibrationConfidence
                + '}';
    }

    /**
     * Builder for ExposureWindow.
     */
    public static class Builder {

        private long dateMillisSinceEpoch;
        private List<ScanInstance> scanInstances;
        @ReportType
        private int reportType;
        @Infectiousness
        private int infectiousness;
        @CalibrationConfidence
        private int calibrationConfidence;

        public Builder() {
            dateMillisSinceEpoch = 0;
            scanInstances = ImmutableList.of();
            reportType = ReportType.CONFIRMED_TEST;
            infectiousness = Infectiousness.STANDARD;
            calibrationConfidence = CalibrationConfidence.LOWEST;
        }

        public Builder setDateMillisSinceEpoch(long dateMillisSinceEpoch) {
            this.dateMillisSinceEpoch = dateMillisSinceEpoch;
            return this;
        }

        public Builder setScanInstances(List<ScanInstance> scanInstances) {
            this.scanInstances = Preconditions.checkNotNull(scanInstances);
            return this;
        }

        public Builder setReportType(@ReportType int reportType) {
            checkArgument(
                    reportType > ReportType.UNKNOWN && reportType < ReportType.REVOKED,
                    "reportType (%d) is not allowed",
                    reportType);
            this.reportType = reportType;
            return this;
        }

        public Builder setInfectiousness(@Infectiousness int infectiousness) {
            Preconditions.checkNotNull(
                    ExposureNotificationEnums.Infectiousness.forNumber(infectiousness),
                    String.format(Locale.getDefault(), "infectiousness (%d) is invalid", infectiousness));
            this.infectiousness = infectiousness;
            return this;
        }

        public Builder setCalibrationConfidence(@CalibrationConfidence int calibrationConfidence) {
            checkNotNull(
                    ExposureNotificationEnums.CalibrationConfidence.forNumber(calibrationConfidence),
                    String.format(
                            Locale.getDefault(), "calibrationConfidence (%d) is invalid", calibrationConfidence));
            this.calibrationConfidence = calibrationConfidence;
            return this;
        }

        public ExposureWindow build() {
            return new ExposureWindow(
                    dateMillisSinceEpoch, scanInstances, reportType, infectiousness, calibrationConfidence);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.IntDef;

import com.google.samples.exposurenotification.ExposureNotificationEnums;

/**
 * Infectiousness defined for an {@link ExposureWindow}.
 */
@IntDef({
        Infectiousness.NONE,
        Infectiousness.STANDARD,
        Infectiousness.HIGH,
})
public @interface Infectiousness {
    int NONE = ExposureNotificationEnums.Infectiousness.INFECTIOUSNESS_NONE_VALUE;
    int STANDARD = ExposureNotificationEnums.Infectiousness.INFECTIOUSNESS_STANDARD_VALUE;
    int HIGH = ExposureNotificationEnums.Infectiousness.INFECTIOUSNESS_HIGH_VALUE;
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import android.os.Bundle;

import androidx.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.samples.exposurenotification.safeparcel.AbstractSafeParcelable;
import com.google.samples.exposurenotification.safeparcel.SafeParcelable;

import java.util.Locale;

/**
 * Holds configuration values that can be passed onto the client app after it has finished
 * installing via {@link ExposureNotificationClient#getPackageConfiguration()}.
 */
@SafeParcelable.Class(creator = "PackageConfigurationCreator")
public final class PackageConfiguration extends AbstractSafeParcelable {

    Bundle values;

    @Constructor
    PackageConfiguration(@Param(id = 1) Bundle values) {
        this.values = values;
    }

    /** Returns a Bundle containing all configuration options. */
    public Bundle getValues() {
        return values;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof PackageConfiguration) {
            PackageConfiguration that = (PackageConfiguration) obj;
            return Objects.equal(values, that.getValues());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(values);
    }

    @Override
    public String toString() {
        return String.format(Locale.US, "PackageConfiguration<values: %s>", values);
    }

    /** A builder for {@link PackageConfiguration}. */
    public static final class PackageConfigurationBuilder {

        private Bundle values;

        /** Sets a Bundle containing configuration options. */
        public PackageConfigurationBuilder setValues(Bundle values) {
            this.values = new Bundle(values);
            return this;
        }

        /** Builds a {@link PackageConfiguration}. */
        public PackageConfiguration build() {
            return new PackageConfiguration(values);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.IntDef;

/**
 * Report type defined for a {@link TemporaryExposureKey}.
 */
@IntDef({
        ReportType.UNKNOWN,
        ReportType.CONFIRMED_TEST,
        ReportType.CONFIRMED_CLINICAL_DIAGNOSIS,
        ReportType.SELF_REPORT,
        ReportType.RECURSIVE,
        ReportType.REVOKED,
})
public @interface ReportType {
    int UNKNOWN = 0;
    int CONFIRMED_TEST = 1;
    int CONFIRMED_CLINICAL_DIAGNOSIS = 2;
    int SELF_REPORT = 3;
    int RECURSIVE = 4;
    int REVOKED = 5;
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.IntDef;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Risk level defined for an {@link TemporaryExposureKey}.
 */
@IntDef({
        RiskLevel.RISK_LEVEL_INVALID,
        RiskLevel.RISK_LEVEL_LOWEST,
        RiskLevel.RISK_LEVEL_LOW,
        RiskLevel.RISK_LEVEL_LOW_MEDIUM,
        RiskLevel.RISK_LEVEL_MEDIUM,
        RiskLevel.RISK_LEVEL_MEDIUM_HIGH,
        RiskLevel.RISK_LEVEL_HIGH,
        RiskLevel.RISK_LEVEL_VERY_HIGH,
        RiskLevel.RISK_LEVEL_HIGHEST,
})
@Retention(RetentionPolicy.SOURCE)
public @interface RiskLevel {
    int RISK_LEVEL_INVALID = 0;
    int RISK_LEVEL_LOWEST = 1;
    int RISK_LEVEL_LOW = 2;
    int RISK_LEVEL_LOW_MEDIUM = 3;
    int RISK_LEVEL_MEDIUM = 4;
    int RISK_LEVEL_MEDIUM_HIGH = 5;
    int RISK_LEVEL_HIGH = 6;
    int RISK_LEVEL_VERY_HIGH = 7;
    int RISK_LEVEL_HIGHEST = 8;
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;


import com.google.common.base.Objects;

/**
 * Information about the sighting of a TEK within a BLE scan (of a few seconds).
 *
 * <p>The TEK itself isn't exposed by the API.
 */
public final class ScanInstance {

    int typicalAttenuationDb;
    int minAttenuationDb;
    int secondsSinceLastScan;

    ScanInstance(
            int typicalAttenuationDb,
            int minAttenuationDb,
            int secondsSinceLastScan) {
        this.typicalAttenuationDb = typicalAttenuationDb;
        this.minAttenuationDb = minAttenuationDb;
        this.secondsSinceLastScan = secondsSinceLastScan;
    }

    /**
     * Aggregation of the attenuations of all of this TEK's beacons received during the scan, in dB.
     * This is most likely to be an average in the dB domain.
     */
    public int getTypicalAttenuationDb() {
        return typicalAttenuationDb;
    }

    /**
     * Minimum attenuation of all of this TEK's beacons received during the scan, in dB.
     */
    public int getMinAttenuationDb() {
        return minAttenuationDb;
    }

    /**
     * Seconds elapsed since the previous scan, typically used as a weight.
     *
     * <p>Two example uses: - Summing those values over all sightings of an exposure provides the
     * duration of that exposure. - Summing those values over all sightings in a given attenuation
     * range and over all exposures recreates the durationAtBuckets of v1.
     *
     * <p>Note that the previous scan may not have led to a sighting of that TEK.
     */
    public int getSecondsSinceLastScan() {
        return secondsSinceLastScan;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ScanInstance that = (ScanInstance) o;
        return typicalAttenuationDb == that.typicalAttenuationDb
                && minAttenuationDb == that.minAttenuationDb
                && secondsSinceLastScan == that.secondsSinceLastScan;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(typicalAttenuationDb, minAttenuationDb, secondsSinceLastScan);
    }

    @Override
    public String toString() {
        return "ScanInstance{"
                + "typicalAttenuationDb="
                + typicalAttenuationDb
                + ", minAttenuationDb="
                + minAttenuationDb
                + ", secondsSinceLastScan="
                + secondsSinceLastScan
                + '}';
    }

    /**
     * Builder for ScanInstance.
     */
    public static class Builder {

        private int typicalAttenuationDb;
        private int minAttenuationDb;
        private int secondsSinceLastScan;

        public Builder() {
            typicalAttenuationDb = 0;
            minAttenuationDb = 0;
            secondsSinceLastScan = 0;
        }

        public Builder setTypicalAttenuationDb(int typicalAttenuationDb) {
            this.typicalAttenuationDb = typicalAttenuationDb;
            return this;
        }

        public Builder setMinAttenuationDb(int minAttenuationDb) {
            this.minAttenuationDb = minAttenuationDb;
            return this;
        }

        public Builder setSecondsSinceLastScan(int secondsSinceLastScan) {
            this.secondsSinceLastScan = secondsSinceLastScan;
            return this;
        }

        public ScanInstance build() {
            return new ScanInstance(typicalAttenuationDb, minAttenuationDb, secondsSinceLastScan);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import androidx.annotation.Nullable;

import com.google.common.base.Objects;
import com.google.samples.Hex;

import java.util.Arrays;
import java.util.Date;
import java.util.Locale;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * A scanned packet coming from other device's EN advertising.
 *
 * @hide
 */
public class ScannedPacket {

    byte[] id;
    byte[] encryptedMetadata;
    ScannedPacketContent[] scannedPacketContents;

    ScannedPacket(
            byte[] id,
            byte[] encryptedMetadata,
            ScannedPacketContent[] scannedPacketContents) {
        this.id = id;
        this.encryptedMetadata = encryptedMetadata;
        this.scannedPacketContents = scannedPacketContents;
    }

    /**
     * Returns the rolling proximity ID.
     */
    public byte[] getId() {
        return Arrays.copyOf(id, id.length);
    }

    /**
     * Returns the encrypted metadata.
     */
    public byte[] getEncryptedMetadata() {
        return Arrays.copyOf(encryptedMetadata, encryptedMetadata.length);
    }

    /**
     * Returns the scanned packet contents.
     */
    public ScannedPacketContent[] getScannedPacketContents() {
        return Arrays.copyOf(scannedPacketContents, scannedPacketContents.length);
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof ScannedPacket) {
            ScannedPacket that = (ScannedPacket) obj;
            return Arrays.equals(id, that.id)
                    && Arrays.equals(encryptedMetadata, that.encryptedMetadata)
                    && Arrays.equals(scannedPacketContents, that.scannedPacketContents);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id, encryptedMetadata, scannedPacketContents);
    }

    @Override
    public String toString() {
        StringBuilder scannedPacketContentsString = new StringBuilder();
        for (ScannedPacketContent scannedPacketContent : scannedPacketContents) {
            scannedPacketContentsString.append(scannedPacketContent.toString()).append(",");
        }
        return String.format(
                Locale.US,
                "ScannedPacket<id: %s, encryptedMetadata: %s, scannedPacketContents: %s>",
                Hex.bytesToStringLowercase(id),
                Hex.bytesToStringLowercase(encryptedMetadata),
                scannedPacketContentsString);
    }

    /**
     * A builder for {@link ScannedPacket}.
     */
    public static final class ScannedPacketBuilder {
        private byte[] id = new byte[0];
        private byte[] encryptedMetadata = new byte[0];
        private ScannedPacketContent[] scannedPacketContents = new ScannedPacketContent[0];

        public ScannedPacketBuilder setId(byte[] id) {
            this.id = Arrays.copyOf(id, id.length);
            return this;
        }

        public ScannedPacketBuilder setEncryptedMetadata(byte[] encryptedMetadata) {
            this.encryptedMetadata = Arrays.copyOf(encryptedMetadata, encryptedMetadata.length);
            return this;
        }

        public ScannedPacketBuilder setScannedPacketContents(
                ScannedPacketContent[] scannedPacketContents) {
            checkArgument(
                    scannedPacketContents != null && scannedPacketContents.length > 0,
                    "scannedPacketContents's size must be > 0");
            this.scannedPacketContents =
                    Arrays.copyOf(scannedPacketContents, scannedPacketContents.length);
            return this;
        }

        public ScannedPacket build() {
            return new ScannedPacket(id, encryptedMetadata, scannedPacketContents);
        }
    }

    /**
     * A scanned packet content coming from other device's EN advertising.
     *
     * @hide
     */
    public static class ScannedPacketContent {

        int epochSeconds;
        int rssi;
        int previousScanEpochSeconds;

        ScannedPacketContent(
                int epochSeconds,
                int rssi,
                int previousScanEpochSeconds) {
            this.epochSeconds = epochSeconds;
            this.rssi = rssi;
            this.previousScanEpochSeconds = previousScanEpochSeconds;
        }

        public int getEpochSeconds() {
            return epochSeconds;
        }

        public int getRssi() {
            return rssi;
        }

        public int getPreviousScanEpochSeconds() {
            return previousScanEpochSeconds;
        }

        @Override
        public boolean equals(@Nullable Object obj) {
            if (obj instanceof ScannedPacketContent) {
                ScannedPacketContent that = (ScannedPacketContent) obj;
                return Objects.equal(epochSeconds, that.epochSeconds)
                        && Objects.equal(rssi, that.rssi)
                        && Objects.equal(previousScanEpochSeconds, that.previousScanEpochSeconds);
            }
            return false;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(epochSeconds, rssi, previousScanEpochSeconds);
        }

        @Override
        public String toString() {
            return String.format(
                    Locale.US,
                    "ScannedPacketContent<"
                            + "epochSeconds: %s, "
                            + "rssi: %d, "
                            + "previousScanEpochSeconds: %s>",
                    new Date(SECONDS.toMillis(epochSeconds)),
                    rssi,
                    new Date(SECONDS.toMillis(previousScanEpochSeconds)));
        }

        /**
         * A builder for {@link ScannedPacketContent}.
         */
        public static final class ScannedPacketContentBuilder {
            private int epochSeconds = 0;
            private int rssi = 0;
            private int previousScanEpochSeconds = 0;

            public ScannedPacketContentBuilder setEpochSeconds(int epochSeconds) {
                this.epochSeconds = epochSeconds;
                return this;
            }

            public ScannedPacketContentBuilder setRssi(int rssi) {
                this.rssi = rssi;
                return this;
            }

            public ScannedPacketContentBuilder setPreviousScanEpochSeconds(int previousScanEpochSeconds) {
                this.previousScanEpochSeconds = previousScanEpochSeconds;
                return this;
            }

            public ScannedPacketContent build() {
                return new ScannedPacketContent(epochSeconds, rssi, previousScanEpochSeconds);
            }
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import android.os.Parcel;
import android.os.Parcelable;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.concurrent.TimeUnit.MINUTES;

/**
 * A key generated for advertising over a window of time.
 */
public final class TemporaryExposureKey implements Parcelable {
    /**
     * The default value for {@link #getDaysSinceOnsetOfSymptoms}.
     *
     * <p>See {@link DiagnosisKeysDataMapping#getDaysSinceOnsetToInfectiousness} for more information.
     */
    public static final int DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN = Integer.MAX_VALUE;

    public static final Parcelable.Creator<TemporaryExposureKey> CREATOR =
            new Parcelable.Creator<TemporaryExposureKey>() {

                @Override
                public TemporaryExposureKey createFromParcel(Parcel source) {
                    return new TemporaryExposureKey(source);
                }

                @Override
                public TemporaryExposureKey[] newArray(int size) {
                    return new TemporaryExposureKey[size];
                }
            };

    byte[] keyData;
    int rollingStartIntervalNumber;
    @RiskLevel
    int transmissionRiskLevel;
    int rollingPeriod;
    @ReportType
    int reportType;
    int daysSinceOnsetOfSymptoms;

    TemporaryExposureKey(
            byte[] keyData,
            int rollingStartIntervalNumber,
            @RiskLevel int transmissionRiskLevel,
            int rollingPeriod,
            @ReportType int reportType,
            int daysSinceOnsetOfSymptoms) {
        this.keyData = keyData;
        this.rollingStartIntervalNumber = rollingStartIntervalNumber;
        this.transmissionRiskLevel = transmissionRiskLevel;
        this.rollingPeriod = rollingPeriod;
        this.reportType = reportType;
        this.daysSinceOnsetOfSymptoms = daysSinceOnsetOfSymptoms;
    }

    TemporaryExposureKey(
            byte[] keyData,
            int rollingStartIntervalNumber,
            @RiskLevel int transmissionRiskLevel,
            int rollingPeriod,
            int daysSinceOnsetOfSymptoms) {
        this.keyData = keyData;
        this.rollingStartIntervalNumber = rollingStartIntervalNumber;
        this.transmissionRiskLevel = transmissionRiskLevel;
        this.rollingPeriod = rollingPeriod;
        this.daysSinceOnsetOfSymptoms = daysSinceOnsetOfSymptoms;
    }

    private TemporaryExposureKey(@NonNull Parcel in) {
        int keyLen = in.readInt();
        this.keyData = new byte[keyLen];
        in.readByteArray(this.keyData);
        this.rollingStartIntervalNumber = in.readInt();
        this.transmissionRiskLevel = in.readInt();
        this.rollingPeriod = in.readInt();
        this.daysSinceOnsetOfSymptoms = in.readInt();
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(this.keyData.length);
        dest.writeByteArray(this.keyData);
        dest.writeInt(this.rollingStartIntervalNumber);
        dest.writeInt(this.transmissionRiskLevel);
        dest.writeInt(this.rollingPeriod);
        dest.writeInt(this.daysSinceOnsetOfSymptoms);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * The randomly generated Temporary Exposure Key information.
     */
    public byte[] getKeyData() {
        return Arrays.copyOf(keyData, keyData.length);
    }

    /**
     * A number describing when a key starts. It is equal to startTimeOfKeySinceEpochInSecs / (60 *
     * 10).
     */
    public int getRollingStartIntervalNumber() {
        return rollingStartIntervalNumber;
    }

    /**
     * Risk of transmission associated with the person this key came from.
     */
    @RiskLevel
    public int getTransmissionRiskLevel() {
        return transmissionRiskLevel;
    }

    /**
     * A number describing how long a key is valid. It is expressed in increments of 10 minutes (e.g.
     * 144 for 24 hours).
     */
    public int getRollingPeriod() {
        return rollingPeriod;
    }

    /**
     * Type of diagnosis associated with a key.
     */
    @ReportType
    public int getReportType() {
        return reportType;
    }

    /**
     * Number of days elapsed between symptom onset and the key being used.
     *
     * <p>E.g. 2 means the key is 2 days after onset of symptoms.
     */
    public int getDaysSinceOnsetOfSymptoms() {
        return daysSinceOnsetOfSymptoms;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        if (obj instanceof TemporaryExposureKey) {
            TemporaryExposureKey that = (TemporaryExposureKey) obj;
            return Arrays.equals(keyData, that.getKeyData())
                    && Objects.equals(rollingStartIntervalNumber, that.getRollingStartIntervalNumber())
                    && Objects.equals(transmissionRiskLevel, that.getTransmissionRiskLevel())
                    && Objects.equals(rollingPeriod, that.getRollingPeriod())
                    && Objects.equals(daysSinceOnsetOfSymptoms, that.getDaysSinceOnsetOfSymptoms());
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hash(keyData, rollingStartIntervalNumber, transmissionRiskLevel,
                rollingPeriod, daysSinceOnsetOfSymptoms);
    }

    @Override
    public String toString() {
        return String.format(
                Locale.US,
                "TemporaryExposureKey<"
                        + "keyData: %s, "
                        + "rollingStartIntervalNumber: %s, "
                        + "transmissionRiskLevel: %d, "
                        + "rollingPeriod: %d, "
                        + "daysSinceOnsetOfSymptoms: %d"
                        + ">",
                new BigInteger(1, keyData).toString(16),
                new Date(MINUTES.toMillis(rollingStartIntervalNumber * 10L)),
                transmissionRiskLevel,
                rollingPeriod,
                daysSinceOnsetOfSymptoms);
    }

    /**
     * A builder for {@link TemporaryExposureKey}.
     */
    public static final class TemporaryExposureKeyBuilder {

        private byte[] keyData = new byte[0];
        private int rollingStartIntervalNumber = 0;
        @RiskLevel
        private int transmissionRiskLevel = RiskLevel.RISK_LEVEL_INVALID;
        private int rollingPeriod = 1;
        @ReportType
        private int reportType = ReportType.UNKNOWN;
        private int daysSinceOnsetOfSymptoms = DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN;

        public TemporaryExposureKeyBuilder setKeyData(byte[] keyData) {
            this.keyData = Arrays.copyOf(keyData, keyData.length);
            return this;
        }

        public TemporaryExposureKeyBuilder setRollingStartIntervalNumber(
                int rollingStartIntervalNumber) {
            checkArgument(
                    rollingStartIntervalNumber >= 0,
                    "rollingStartIntervalNumber (%s) must be >= 0",
                    rollingStartIntervalNumber);
            this.rollingStartIntervalNumber = rollingStartIntervalNumber;
            return this;
        }

        public TemporaryExposureKeyBuilder setTransmissionRiskLevel(
                @RiskLevel int transmissionRiskLevel) {
            checkArgument(
                    transmissionRiskLevel >= 0 && transmissionRiskLevel <= 8,
                    "transmissionRiskLevel (%s) must be >= 0 and <= 8",
                    transmissionRiskLevel);
            this.transmissionRiskLevel = transmissionRiskLevel;
            return this;
        }

        public TemporaryExposureKeyBuilder setRollingPeriod(int rollingPeriod) {
            checkArgument(rollingPeriod > 0, "rollingPeriod (%s) must be > 0", rollingPeriod);
            this.rollingPeriod = rollingPeriod;
            return this;
        }

        public TemporaryExposureKeyBuilder setReportType(@ReportType int reportType) {
            checkNotNull(
                    reportType >= ReportType.UNKNOWN && reportType <= ReportType.REVOKED,
                    String.format(Locale.getDefault(), "reportType (%d) is invalid", reportType));
            this.reportType = reportType;
            return this;
        }

        public TemporaryExposureKeyBuilder setDaysSinceOnsetOfSymptoms(int daysSinceOnsetOfSymptoms) {
            checkArgument(
                    (daysSinceOnsetOfSymptoms >= -14 && daysSinceOnsetOfSymptoms <= 14),
                    "daysSinceOnsetOfSymptoms (%d) must be >= -14 and <= 14",
                    daysSinceOnsetOfSymptoms);
            this.daysSinceOnsetOfSymptoms = daysSinceOnsetOfSymptoms;
            return this;
        }

        public TemporaryExposureKey build() {
            return new TemporaryExposureKey(
                    keyData,
                    rollingStartIntervalNumber,
                    transmissionRiskLevel,
                    rollingPeriod,
                    reportType,
                    daysSinceOnsetOfSymptoms);
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.nearby;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;
import androidx.annotation.Nullable;

import com.google.samples.exposurenotification.annotations.Hide;
import com.google.samples.exposurenotification.threads.TracingHandler;
import com.google.samples.matching.BuildConfig;

/**
 * Bindable service which will be used to wake up a valid Exposure Notification client app in the
 * case where it has been force stopped and is no longer able to provide diagnosis key files in the
 * background.
 *
 * <p>This service does no actual work. It creates a Messenger that can accept messages, but only
 * then logs any messages that it receives. It is expected for this service to be unbound from
 * shortly after being bound.
 *
 * @hide
 */
@Hide
public class WakeUpService extends Service {

    private static final String TAG = "WakeUpService";

    /** Identifies the name of this service for later binding. */
    public static final String SERVICE_NAME =
            "com.google.android.gms.nearby.exposurenotification.WakeUpService";

    /** A simple handler which logs incoming messages. */
    private static class LoggingHandler extends TracingHandler {
        @Override
        public void handleMessage(Message msg) {
            if (BuildConfig.DEBUG) {
                Log.i(TAG, "Received message " + msg.what);
            }
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        if (BuildConfig.DEBUG) {
            Log.i(TAG, "Waking up due to EN binding.");
        }
        return new Messenger(new LoggingHandler()).getBinder();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.safeparcel;

import android.os.Parcel;
import android.os.Parcelable;

/**
 * Implements {@link SafeParcelable} and implements some default methods
 * defined by {@link android.os.Parcelable}.
 *
 * @hide
 */
public abstract class AbstractSafeParcelable implements SafeParcelable {

    public static final Parcelable.Creator<AbstractSafeParcelable> CREATOR = null;

    /** @hide */
    public final int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        // See the following library for a sample implementation of this:
        // https://developer.android.com/reference/androidx/versionedparcelable/VersionedParcelable
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.safeparcel;

import android.os.Parcelable;

/**
 * This interface is used only to preserve compatibility with the annotations used in the Exposure
 * Notifications code, but the actual implementation is not available here because it's not relevant
 * to the usage of Exposure Notifications.
 */
public interface ReflectedParcelable extends Parcelable {}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.safeparcel;

import android.os.Parcelable;

/**
 * This interface is used only to preserve compatibility with the annotations used in the Exposure
 * Notifications code, but the actual implementation is not available here because it's not relevant
 * to the usage of Exposure Notifications.
 */
public interface SafeParcelable extends Parcelable {
    /** @hide */
    public static final String NULL = "SAFE_PARCELABLE_NULL_STRING";

    /** @hide */
    public @interface Class {
        String creator();
        boolean validate() default false;
    }

    /** @hide */
    public @interface Field {
        int id();
        String getter() default NULL;
        String type() default NULL;
        String defaultValue() default NULL;
        String defaultValueUnchecked() default NULL;
    }

    /** @hide */
    public @interface VersionField {
        int id();
        String getter() default NULL;
        String type() default NULL;
    }

    /** @hide */
    public @interface Indicator {
        String getter() default NULL;
    }

    /** @hide */
    public @interface Constructor { }

    /** @hide */
    public @interface Param {
        int id();
    }

    /** @hide */
    public @interface Reserved {
        int[] value();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

import java.io.Closeable;

/**
 * An {@link Iterable} that can be (sometimes must be) closed to release resource after use.
 */
public interface CloseableIterable<T> extends Iterable<T>, Closeable {
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

import androidx.annotation.NonNull;

import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

/**
 * Provides utility functions for {@link CloseableIterable}.
 */
public final class CloseableIterables {
    /**
     * A simple implementation of concatenating multiple {@link CloseableIterable}. The returned
     * iterable will iterate the first {@link CloseableIterable} in {@code in iterables} and then the
     * second, etc.
     */
    public static <T> CloseableIterable<T> concat(List<CloseableIterable<T>> closeableIterables) {
        if (closeableIterables.isEmpty()) {
            return emptyInstance();
        }

        return new CloseableIterable<T>() {
            @NonNull
            @Override
            public Iterator<T> iterator() {
                return Iterators.concat(
                        Iterables.transform(closeableIterables, CloseableIterable::iterator).iterator());
            }

            @Override
            public void close() throws IOException {
                for (CloseableIterable<T> closeableIterable : closeableIterables) {
                    closeableIterable.close();
                }
            }
        };
    }

    public static <T> CloseableIterable<T> emptyInstance() {
        return new CloseableIterable<T>() {
            @Override
            public void close() throws IOException {
            }

            @NonNull
            @Override
            public Iterator<T> iterator() {
                return new Iterator<T>() {
                    @Override
                    public boolean hasNext() {
                        return false;
                    }

                    @Override
                    public T next() {
                        throw new NoSuchElementException();
                    }
                };
            }
        };
    }

    private CloseableIterables() {
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

import android.content.Context;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.google.common.collect.Iterables;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.ScannedPacket;
import com.google.samples.exposurenotification.nearby.ScannedPacket.ScannedPacketBuilder;
import com.google.samples.exposurenotification.nearby.ScannedPacket.ScannedPacketContent;
import com.google.samples.exposurenotification.nearby.ScannedPacket.ScannedPacketContent.ScannedPacketContentBuilder;
import com.google.samples.exposurenotification.data.DayNumber;
import com.google.samples.exposurenotification.data.RollingProximityId;
import com.google.samples.exposurenotification.features.ContactTracingFeature;

import org.joda.time.Instant;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * The data store for contact records. Contact records are bundled BLE scan results stored in
 * key-value pairs. The key is a concatenation of {@link DayNumber} and {@link RollingProximityId}.
 * The value is a {@link ContactRecordValue};
 */
@SuppressWarnings("NewApi")
public class ContactRecordDataStore implements AutoCloseable {

    private final Map<byte[], byte[]> store = new HashMap<>();

    private ContactRecordDataStore(Context context) throws StorageException {
    }

    public static synchronized ContactRecordDataStore open(Context context) throws StorageException {
        return new ContactRecordDataStore(context);
    }

    @Override
    public synchronized void close() {
    }

    /**
     * Gets the contact records that match the given {@code dayNumber} and {@code rollingProximityId}.
     *
     * @return the record if a match is found, or null otherwise.
     */
    @Nullable
    public ContactRecord getRecord(DayNumber dayNumber, RollingProximityId rollingProximityId) {
        ContactRecordKey key = new ContactRecordKey(dayNumber, rollingProximityId);
        try {
            byte[] valueBytes = store.get(key);
            if (valueBytes == null) {
                return null;
            }
            ContactRecordValue parsedValue = ContactRecordValue.parseFrom(valueBytes);
            return new ContactRecord(
                    key,
                    ContactTracingFeature.contactRecordStoreCompactFormatEnabled()
                            ? convertToCompactFormat(parsedValue)
                            : parsedValue);
        } catch (InvalidProtocolBufferException e) {
            Log.log.atSevere().withCause(e).log("Error getting record");
        }
        return null;
    }

    public List<ContactRecord> getAllRecords() {
        List<ContactRecord> records = new ArrayList<>();
        synchronized (store) {
            try {
                for (Entry<byte[], byte[]> iterator : store.entrySet()) {
                    if (iterator.getKey() == null) {
                        continue;
                    }
                    ContactRecordValue parsedValue =
                            ContactRecordValue.parseFrom(iterator.getValue());
                    records.add(
                            new ContactRecord(
                                    ContactRecordKey.fromBytes(iterator.getKey()),
                                    ContactTracingFeature.contactRecordStoreCompactFormatEnabled()
                                            ? convertToCompactFormat(parsedValue)
                                            : parsedValue));
                }
            } catch (InvalidProtocolBufferException e) {
                Log.log.atSevere().withCause(e).log("Error fetching record");
            }
        }
        return records;
    }

    /**
     * Gets all scan records in {@link ScannedPacket} format, to pass the records from wearable to
     * phone. Should be used in wearable EN module only.
     */
    public List<ScannedPacket> getAllRecordsFromWearable() {
        ArrayList<ScannedPacket> scannedPacketArrayList = new ArrayList<>();
        List<ContactRecord> contactRecordList = getAllRecords();
        Log.log
                .atInfo()
                .log(
                        "getAllRecordsFormWearable called, contactRecordList.size=%d",
                        contactRecordList.size());
        for (ContactRecord contactRecord : contactRecordList) {
            if (contactRecord.getValue().getSightingRecordsCount() <= 0) {
                continue;
            }
            ArrayList<ScannedPacketContent> scannedPacketContents = new ArrayList<>();
            byte[] metadata = null;
            for (SightingRecord sightingRecord : contactRecord.value.getSightingRecordsList()) {
                if (metadata == null) {
                    metadata = sightingRecord.getAssociatedEncryptedMetadata().toByteArray();
                }
                scannedPacketContents.add(
                        new ScannedPacketContentBuilder()
                                .setEpochSeconds(sightingRecord.getEpochSeconds())
                                .setRssi(sightingRecord.getRssi())
                                .setPreviousScanEpochSeconds(sightingRecord.getPreviousScanEpochSeconds())
                                .build());
            }
            if (metadata == null) {
                continue;
            }
            scannedPacketArrayList.add(
                    new ScannedPacketBuilder()
                            .setId(contactRecord.key.rollingProximityId.getDirect())
                            .setEncryptedMetadata(metadata)
                            .setScannedPacketContents(scannedPacketContents.toArray(new ScannedPacketContent[0]))
                            .build());
        }
        Log.log
                .atInfo()
                .log(
                        "getAllRecordsFormWearable done, %d scanned packet found",
                        scannedPacketArrayList.size());
        return scannedPacketArrayList;
    }

    /**
     * Gets all the scanned IDs, for each contact record, only return the 16-byte raw ID.
     */
    public List<byte[]> getAllRawIds() {
        List<byte[]> rawIds = new ArrayList<>();
        synchronized (store) {
            for (Entry<byte[], byte[]> iterator : store.entrySet()) {
                if (iterator.getKey() == null) {
                    continue;
                }
                rawIds.add(ContactRecordKey.getRollingProximityId(iterator.getKey()));
            }
        }
        return rawIds;
    }

    /**
     * Adds or updates a contact record value with the key given by {@code dayNumber} and {@code
     * rollingProximityId}.
     */
    @VisibleForTesting
    public void putRecord(
            DayNumber dayNumber, RollingProximityId rollingProximityId, ContactRecordValue value) {
        ContactRecordKey key = new ContactRecordKey(dayNumber, rollingProximityId);
        synchronized (store) {
            store.put(key.getBytes(), value.toByteArray());
        }
    }

    /**
     * Appends a {@link SightingRecord} to the {@link ContactRecordValue} keyed by the {@code
     * dayNumber} and {@code rollingProximityId}.
     */
    public void appendSightingRecord(
            Instant sightTime,
            byte[] id,
            int rssi,
            byte[] associatedEncryptedMetadata,
            int previousScanEpochSeconds) {
        DayNumber dayNumber = new DayNumber(sightTime);
        int sightTimeSeconds = (int) MILLISECONDS.toSeconds(sightTime.getMillis());
        RollingProximityId rollingProximityId = new RollingProximityId(id);

        if (!ContactTracingFeature.contactRecordStoreCompactFormatEnabled()) {
            SightingRecord sightingRecord =
                    SightingRecord.newBuilder()
                            .setEpochSeconds(sightTimeSeconds)
                            .setRssi(rssi)
                            .setAssociatedEncryptedMetadata(ByteString.copyFrom(associatedEncryptedMetadata))
                            .setPreviousScanEpochSeconds(previousScanEpochSeconds)
                            .build();
            ContactRecord contactRecord = getRecord(dayNumber, rollingProximityId);
            ContactRecordValue contactRecordValue =
                    contactRecord == null
                            ? ContactRecordValue.getDefaultInstance()
                            : contactRecord.getValue();
            ContactRecordValue updatedContactRecordValue =
                    contactRecordValue.toBuilder().addSightingRecords(sightingRecord).build();
            putRecord(dayNumber, rollingProximityId, updatedContactRecordValue);
            return;
        }

        ContactRecord contactRecord = getRecord(dayNumber, rollingProximityId);
        ContactRecordValue existingValue =
                contactRecord == null ? null : convertToCompactFormat(contactRecord.getValue());
        ContactRecordValue updatedValue;
        if (existingValue == null || existingValue.getSightingRecordsCount() == 0) {
            // No records for this RPI, or the data is corrupted. We create new ContactRecordValue
            // completely.
            updatedValue =
                    ContactRecordValue.newBuilder()
                            .setEncryptedMetadata(ByteString.copyFrom(associatedEncryptedMetadata))
                            .addSightingRecords(
                                    SightingRecord.newBuilder()
                                            .setEpochSeconds(sightTimeSeconds)
                                            .setPreviousScanEpochSeconds(previousScanEpochSeconds)
                                            .setRssiValues(wrapSingleRssi(rssi)))
                            .build();
        } else {
            SightingRecord.Builder sightingRecordBuilder =
                    Iterables.getLast(existingValue.getSightingRecordsList()).toBuilder();
            if (isSameScanCycle(sightingRecordBuilder, sightTimeSeconds)) {
                sightingRecordBuilder.setRssiValues(
                        sightingRecordBuilder.getRssiValues().concat(wrapSingleRssi(rssi)));
                backFillEncryptedMetadataIfRequired(
                        sightingRecordBuilder,
                        existingValue.getEncryptedMetadata(),
                        ByteString.copyFrom(associatedEncryptedMetadata));
                updatedValue =
                        existingValue.toBuilder()
                                .setSightingRecords(
                                        existingValue.getSightingRecordsCount() - 1, sightingRecordBuilder)
                                .build();
            } else {
                updatedValue =
                        existingValue.toBuilder()
                                .addSightingRecords(
                                        SightingRecord.newBuilder()
                                                .setEpochSeconds(sightTimeSeconds)
                                                .setPreviousScanEpochSeconds(previousScanEpochSeconds)
                                                .setRssiValues(wrapSingleRssi(rssi)))
                                .build();
            }
        }

        putRecord(dayNumber, rollingProximityId, updatedValue);
    }

    private static boolean isSameScanCycle(
            SightingRecord.Builder sightingRecordBuilder, int sightingTimeSeconds) {
        // Simple heuristic that considering a new sighting packet to be in the same scan cycle of a
        // SightingRecord if the elapsed seconds since the beginning of SightingRecord is no bigger than
        // 1.5x maximum possible scan duration.
        return sightingTimeSeconds
                <= sightingRecordBuilder.getEpochSeconds()
                + (ContactTracingFeature.scanTimeSeconds()
                + ContactTracingFeature.scanTimeExtendForProfileInUseSeconds())
                * 1.5;
    }

    /**
     * Ensures that `sightingRecordBuilder.encryptedMetadata` has the same (or larger) length as
     * `sightingRecordBuilder.rssiValues`. If not, fill the slots with the `encryptedMetadata`.
     */
    private static void fillMissingEncryptedMetadata(
            SightingRecord.Builder sightingRecordBuilder, ByteString encryptedMetadata) {
        if (!sightingRecordBuilder.hasRssiValues()) {
            return;
        }
        while (sightingRecordBuilder.getEncryptedMetadataCount()
                < sightingRecordBuilder.getRssiValues().size()) {
            sightingRecordBuilder.addEncryptedMetadata(encryptedMetadata);
        }
    }

    @VisibleForTesting
    static ByteString wrapSingleRssi(int rssi) {
        return ByteString.copyFrom(new byte[]{(byte) rssi});
    }

    /**
     * Back fills {@link SightingRecord.Builder#getEncryptedMetadataList()} per format requirement: If
     * {@code currentEncryptedMetadata} is not equal to {@code baseEncryptedMetadata}, we must fill
     * encrypted metadata for this sighting packet as well as all previous packets if absent. If not
     * equal, we do not need to fill it as long as the whole {@link
     * SightingRecord.Builder#getEncryptedMetadataList()} is empty, otherwise we still need to set it.
     *
     * @param sightingRecordBuilder    the builder to update
     * @param baseEncryptedMetadata    the encrypted metadata in {@link ContactRecordValue}.
     * @param currentEncryptedMetadata the encrypted metadata associated with the last element of
     *                                 {@link SightingRecord.Builder#getRssiValues()}.
     */
    private static void backFillEncryptedMetadataIfRequired(
            SightingRecord.Builder sightingRecordBuilder,
            ByteString baseEncryptedMetadata,
            ByteString currentEncryptedMetadata) {
        if (sightingRecordBuilder.getEncryptedMetadataCount() != 0
                || !currentEncryptedMetadata.equals(baseEncryptedMetadata)) {
            fillMissingEncryptedMetadata(sightingRecordBuilder, baseEncryptedMetadata);
            sightingRecordBuilder.setEncryptedMetadata(
                    sightingRecordBuilder.getEncryptedMetadataCount() - 1, currentEncryptedMetadata);
        }
    }

    /**
     * Coverts a {@link ContactRecordValue} to the "compact format" if it is not already in that
     * format.
     *
     * <p>We can remove the conversion code once the build using compact format has been rolled out
     * for a while.
     */
    private static ContactRecordValue convertToCompactFormat(ContactRecordValue originalValue) {
        if (originalValue.hasEncryptedMetadata()) {
            return originalValue;
        }

        ContactRecordValue.Builder updatedValueBuilder = ContactRecordValue.newBuilder();
        SightingRecord.Builder sightingRecordBuilder = null;
        for (SightingRecord sightingRecord : originalValue.getSightingRecordsList()) {
            // Set the base encrypted metadata to be the first one we see from the list.
            if (!updatedValueBuilder.hasEncryptedMetadata()) {
                updatedValueBuilder.setEncryptedMetadata(sightingRecord.getAssociatedEncryptedMetadata());
            }

            if (sightingRecordBuilder != null
                    && isSameScanCycle(sightingRecordBuilder, sightingRecord.getEpochSeconds())) {
                sightingRecordBuilder.setRssiValues(
                        sightingRecordBuilder.getRssiValues().concat(wrapSingleRssi(sightingRecord.getRssi())));
            } else {
                if (sightingRecordBuilder != null) {
                    updatedValueBuilder.addSightingRecords(sightingRecordBuilder);
                }
                sightingRecordBuilder =
                        sightingRecord.toBuilder()
                                .setRssiValues(wrapSingleRssi(sightingRecord.getRssi()))
                                .clearAssociatedEncryptedMetadata()
                                .clearRssi();
            }
            backFillEncryptedMetadataIfRequired(
                    sightingRecordBuilder,
                    updatedValueBuilder.getEncryptedMetadata(),
                    sightingRecord.getAssociatedEncryptedMetadata());
        }
        if (sightingRecordBuilder != null) {
            updatedValueBuilder.addSightingRecords(sightingRecordBuilder);
        }

        return updatedValueBuilder.build();
    }

    /**
     * Deletes all contact records on a day given by the {@link DayNumber}.
     *
     * @return the total number of entries deleted
     */
    public int deletesRecords(DayNumber dayNumber) throws StorageException {
        // FIXME: delete records for a given day number.
        return 0;
    }

    /**
     * Deletes all contact records prior (inclusive) to a day given by the {@link DayNumber}.
     *
     * @return the total number of entries deleted
     */
    public int deletePriorRecords(DayNumber lastDayNumberToDelete) throws StorageException {
        // FIXME: Delete prescribed records.
        return 0;
    }

    /**
     * An immutable data class represents a contact record. The key-value pair data can be accessed
     * through {@link #getKey()} and {@link #getValue()}.
     */
    public static class ContactRecord {
        private final ContactRecordKey key;
        private final ContactRecordValue value;

        public ContactRecord(ContactRecordKey key, ContactRecordValue value) {
            this.key = key;
            this.value = value;
        }

        public ContactRecordKey getKey() {
            return key;
        }

        public ContactRecordValue getValue() {
            return value;
        }
    }

    /**
     * An immutable data class represents the key for a {@link ContactRecord}. The byte array
     * representation of this class is used as the key of underlying data store.
     */
    public static class ContactRecordKey {
        private final DayNumber dayNumber;
        private final RollingProximityId rollingProximityId;

        public ContactRecordKey(DayNumber dayNumber, RollingProximityId rollingProximityId) {
            this.dayNumber = dayNumber;
            this.rollingProximityId = rollingProximityId;
        }

        public DayNumber getDayNumber() {
            return dayNumber;
        }

        public RollingProximityId getRollingProximityId() {
            return rollingProximityId;
        }

        private static byte[] getRollingProximityId(byte[] contactRecordKeyBytes) {
            return Arrays.copyOfRange(
                    contactRecordKeyBytes, DayNumber.getSizeBytes(), contactRecordKeyBytes.length);
        }

        /**
         * Gets the byte array representation of the key.
         */
        public byte[] getBytes() {
            ByteBuffer byteBuffer =
                    ByteBuffer.allocate(DayNumber.getSizeBytes() + rollingProximityId.length);

            dayNumber.putIn(byteBuffer);
            rollingProximityId.putIn(byteBuffer);

            return byteBuffer.array();
        }

        public static ContactRecordKey fromBytes(byte[] bytes) {
            ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
            DayNumber dayNumber = DayNumber.getFrom(byteBuffer);
            byte[] rollingProximityIdBytes = new byte[RollingProximityId.MIN_ID.length];
            byteBuffer.get(rollingProximityIdBytes);
            return new ContactRecordKey(dayNumber, new RollingProximityId(rollingProximityIdBytes, true));
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

import com.google.common.collect.ImmutableList;

import java.nio.ByteBuffer;
import java.util.List;

import static com.google.common.hash.Hashing.sha256;

/**
 * An encoder that encodes some input into a byte array.
 *
 * <p>To use, extends this class and create a constructor that accepts an input of any type.
 * Implement {@link #encodeToBuffer(ByteBuffer)} and use the class by instantiating and calling
 * {@link #encode()}. To chain a series of these, see {@link SerialEncoder}.
 */
public abstract class Encoder {

    /**
     * "Encode" or "decode" a byte array for saving or restoring from leveldb. Mainly a dummy wrapper
     * class for easy chaining. See {@link SerialEncoder}.
     */
    public static class BytesEncoder extends Encoder {

        private final byte[] bytes;

        public BytesEncoder(byte[] bytes) {
            this.bytes = bytes;
        }

        @Override
        protected void encodeToBuffer(ByteBuffer byteBuffer) {
            byteBuffer.put(bytes);
        }

        @Override
        public int numBytes() {
            return bytes.length;
        }
    }

    /**
     * An encoder that takes a list of encoders to encode a series of value.
     */
    public static abstract class SerialEncoder extends Encoder {

        private final List<Encoder> encoders;

        protected SerialEncoder(ImmutableList<Encoder> encoders) {
            this.encoders = encoders;
        }

        @Override
        protected void encodeToBuffer(ByteBuffer byteBuffer) {
            for (Encoder encoder : encoders) {
                encoder.encodeToBuffer(byteBuffer);
            }
        }

        @Override
        protected int numBytes() {
            int numBytes = 0;
            for (Encoder encoder : encoders) {
                numBytes += encoder.numBytes();
            }
            return numBytes;
        }
    }

    /**
     * Encodes a {@link String} into a byte array by Sha256ing the {@link
     * java.nio.charset.StandardCharsets#UTF_8} encoding of the input string.
     */
    @SuppressWarnings("UnstableApiUsage")
    public static class Sha256StringEncoder extends Encoder {

        private final String string;

        // TODO(wukevin,enxun): Revisit semantics around Encoder classes.
        public Sha256StringEncoder(String string) {
            this.string = string;
        }

        @Override
        protected void encodeToBuffer(ByteBuffer byteBuffer) {
            byteBuffer.put(sha256().hashUnencodedChars(string).asBytes());
        }

        @Override
        protected int numBytes() {
            return sha256().bits() / 8;
        }
    }

    /**
     * Encodes value with {@link #numBytes()} into {@link ByteBuffer}.
     */
    protected abstract void encodeToBuffer(ByteBuffer byteBuffer);

    /**
     * The number of bytes that will be written when {@link #encodeToBuffer(ByteBuffer)} is called.
     */
    protected abstract int numBytes();

    /**
     * Returns a byte array containing the encoded version {@link Encoder} wrapped values.
     */
    public byte[] encode() {
        ByteBuffer byteBuffer = ByteBuffer.allocate(numBytes());
        encodeToBuffer(byteBuffer);
        return byteBuffer.array();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

/** Encoding helper function for comparing the key. */
public class EncodingComparisonHelper {

    /**
     * Returns true if the {@code root} is a sub array of {@code key}.
     *
     * <p>A null input for root will always return true.
     */
    public static boolean hasRoot(byte[] root, byte[] key) {
        if (root.length > key.length) {
            return false;
        }
        for (int i = 0; i < root.length && i < key.length; ++i) {
            if (root[i] != key[i]) {
                return false;
            }
        }
        return root.length <= key.length;
    }

    private EncodingComparisonHelper() {}
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

import com.google.common.base.Converter;
import com.google.common.primitives.Ints;
import com.google.samples.exposurenotification.nearby.ExposureConfiguration;
import com.google.samples.exposurenotification.nearby.ExposureConfiguration.ExposureConfigurationBuilder;

/**
 * Converts between the Android Parcelable {@link ExposureConfiguration} and the proto {@link
 * ExposureConfigurationProto}.
 *
 * <p>Why this converter? https://developer.android.com/reference/android/os/Parcel.html.
 */
public class ExposureConfigurationConverter
        extends Converter<ExposureConfiguration, ExposureConfigurationProto> {

    @Override
    protected ExposureConfigurationProto doForward(ExposureConfiguration config) {
        return ExposureConfigurationProto.newBuilder()
                .setMinimumRiskScore(config.getMinimumRiskScore())
                .setAttenuationWeight(config.getAttenuationWeight())
                .addAllAttenuationScores(Ints.asList(config.getAttenuationScores()))
                .setDaysSinceLastExposureWeight(config.getDaysSinceLastExposureWeight())
                .addAllDaysSinceLastExposureScores(Ints.asList(config.getDaysSinceLastExposureScores()))
                .setDurationWeight(config.getDurationWeight())
                .addAllDurationScores(Ints.asList(config.getDurationScores()))
                .setTransmissionRiskWeight(config.getTransmissionRiskWeight())
                .addAllTransmissionRiskScores(Ints.asList(config.getTransmissionRiskScores()))
                .addAllDurationAtAttenuationThresholds(
                        Ints.asList(config.getDurationAtAttenuationThresholds()))
                .build();
    }

    @Override
    protected ExposureConfiguration doBackward(ExposureConfigurationProto proto) {
        return new ExposureConfigurationBuilder()
                .setMinimumRiskScore(proto.getMinimumRiskScore())
                .setAttenuationWeight(proto.getAttenuationWeight())
                .setAttenuationScores(Ints.toArray(proto.getAttenuationScoresList()))
                .setDaysSinceLastExposureWeight(proto.getDaysSinceLastExposureWeight())
                .setDaysSinceLastExposureScores(Ints.toArray(proto.getDaysSinceLastExposureScoresList()))
                .setDurationWeight(proto.getDurationWeight())
                .setDurationScores(Ints.toArray(proto.getDurationScoresList()))
                .setTransmissionRiskWeight(proto.getTransmissionRiskWeight())
                .setTransmissionRiskScores(Ints.toArray(proto.getTransmissionRiskScoresList()))
                .setDurationAtAttenuationThresholds(
                        Ints.toArray(proto.getDurationAtAttenuationThresholdsList()))
                .build();
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

import android.content.Context;

import androidx.annotation.Nullable;

import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.data.DayNumber;
import com.google.samples.exposurenotification.features.ContactTracingFeature;
import com.google.samples.exposurenotification.storage.Encoder.SerialEncoder;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Storage database that stores {@link ExposureResult}s.
 *
 * <p>The store is keyed by PackageNameHash:Signature:TokenHash:TemporaryExposureKey. See {@link
 * KeyEncoder} for the details of the encoding.
 */
public class ExposureResultStorage implements AutoCloseable {

    private final List<Row> transactionRows = new ArrayList<>();
    private final Map<byte[], byte[]> threadSafeLevelDb = new HashMap<>();

    public static ExposureResultStorage open(Context context) throws StorageException {
        return new ExposureResultStorage(context);
    }

    private ExposureResultStorage(Context context) throws StorageException {
    }

    /**
     * Checks whether there's a {@link ExposureResult} calculated for the specified input already.
     *
     * <p>See {@link Row} for parameter definitions.
     *
     * @deprecated use hasResult(byte[], byte[]) instead.
     */
    @Deprecated
    public boolean hasResult(
            String packageName,
            byte[] signatureHash,
            String token,
            TemporaryExposureKey temporaryExposureKey) {
        Preconditions.checkArgument(signatureHash.length == 32, "Signature hash not Sha256 length.");
        return threadSafeLevelDb.get(
                new KeyEncoder(packageName, signatureHash, token, temporaryExposureKey.getKeyData())
                        .encode())
                != null;
    }

    /**
     * Checks whether there's a {@link ExposureResult} calculated for the specified input already.
     *
     * <p>See {@link Row} for parameter definitions.
     */
    public boolean hasResult(byte[] tokenRoot, byte[] exposureKey) {
        for (Row row : transactionRows) {
            if (row.tokenRoot() != null
                    && Arrays.equals(row.tokenRoot(), tokenRoot)
                    && Arrays.equals(row.key().getKeyData(), exposureKey)) {
                return true;
            }
        }
        return threadSafeLevelDb.get(new KeyEncoder(tokenRoot, exposureKey).encode()) != null;
    }

    /**
     * Stores a single result. Returns true if successfully stored, false otherwise.
     *
     * @deprecated use {@link #storeResult(byte[], TemporaryExposureKey, ExposureResult, boolean)} instead.
     */
    @Deprecated
    public boolean storeResult(Row row) {
        threadSafeLevelDb.put(new KeyEncoder(row).encode(), row.exposureResult().toByteArray());
        return true;
    }

    /**
     * Stores a single result. Returns true if successfully stored, false otherwise.
     */
    public boolean storeResult(
            byte[] tokenRoot,
            TemporaryExposureKey exposureKey,
            ExposureResult exposureResult,
            boolean holdForTransaction) {
        if (holdForTransaction) {
            transactionRows.add(
                    Row.builder()
                            .setTokenRoot(tokenRoot)
                            .setKey(exposureKey)
                            .setExposureResult(exposureResult)
                            .build());
            return true;
        } else {
            threadSafeLevelDb.put(
                    new KeyEncoder(tokenRoot, exposureKey.getKeyData()).encode(),
                    exposureResult.toByteArray());
            return true;
        }
    }

    /**
     * Commits {@link #storeResult} requests when processed using holdForTransaction, completing the
     * write.
     */
    public void commitStoreResultRequestsForTransaction() {
        storeResults(transactionRows);
        transactionRows.clear();
    }

    /**
     * Stores list of results. Returns true if succeeds, false otherwise.
     */
    public boolean storeResults(List<Row> results) {
        synchronized (threadSafeLevelDb) {
            for (Row row : results) {
                byte[] tokenRoot = row.tokenRoot();
                KeyEncoder keyEncoder =
                        tokenRoot != null
                                ? new KeyEncoder(tokenRoot, row.key().getKeyData())
                                : new KeyEncoder(row);
                threadSafeLevelDb.put(keyEncoder.encode(), row.exposureResult().toByteArray());
            }
            return true;
        }
    }

    /**
     * Returns an {@link ExposureResult} calculated for the specified input already.
     *
     * <p>See {@link Row} for parameter definitions.
     */
    public ExposureResult getResult(byte[] tokenRoot, byte[] exposureKey) {
        try {
            byte[] result = threadSafeLevelDb.get(new KeyEncoder(tokenRoot, exposureKey).encode());
            if (result != null) {
                return ExposureResult.parseFrom(result);
            }
        } catch (InvalidProtocolBufferException e) {
            Log.log.atSevere().withCause(e).log("Error checking for result.");
        }
        return ExposureResult.getDefaultInstance();
    }

    /**
     * Gets the exposure results for specified package and token.
     */
    public List<ExposureResult> getAll(String packageName, byte[] signatureHash, String token)
            throws StorageException {
        Preconditions.checkArgument(signatureHash.length == 32, "Signature hash not Sha256 length.");
        byte[] requestKeyRoot = new TokenRootEncoder(packageName, signatureHash, token).encode();

        ImmutableList.Builder<ExposureResult> exposureResultsBuilder = ImmutableList.builder();
        synchronized (threadSafeLevelDb) {
            for (Entry<byte[], byte[]> iterator : threadSafeLevelDb.entrySet()) {
                if (EncodingComparisonHelper.hasRoot(requestKeyRoot, iterator.getKey())) {
                    try {
                        exposureResultsBuilder.add(ExposureResult.parseFrom(iterator.getValue()));
                    } catch (InvalidProtocolBufferException e) {
                        Log.log.atSevere().withCause(e).log("Unable to parse exposure result for key.");
                    }
                }

            }
        }
        return exposureResultsBuilder.build();
    }

    /**
     * Delete results associated with provided package. Returns number of items deleted.
     */
    public int deleteAll(String packageName, byte[] signatureHash) {
        Preconditions.checkState(signatureHash.length == 32, "Signature hash not Sha256 length.");
        byte[] packageKeyRoot = new PackageRootEncoder(packageName, signatureHash).encode();
        int deleted = 0;
        synchronized (threadSafeLevelDb) {
            Iterator<Entry<byte[], byte[]>> iterator = threadSafeLevelDb.entrySet().iterator();
            while (iterator.hasNext()) {
                Entry<byte[], byte[]> item = iterator.next();
                if (EncodingComparisonHelper.hasRoot(packageKeyRoot, item.getKey())) {
                    threadSafeLevelDb.remove(item.getKey());
                }
            }
        }
        return deleted;
    }

    /**
     * Deletes all {@link ExposureResult}s in store.
     */
    public int deleteAll() {
        final int keysPurged;
        synchronized (threadSafeLevelDb) {
            keysPurged = threadSafeLevelDb.size();
            threadSafeLevelDb.clear();
        }
        return keysPurged;
    }

    /**
     * Delete results up to and including {@code lastDayToDelete}.
     *
     * <p>Returns number of results purged.
     */
    public int deletePrior(DayNumber lastDayToDelete) {
        // FIXME: Delete prescribed results.
        return 0;
    }

    @Override
    public void close() {
        /* nothing to close */
    }

    /**
     * A row in {@link ExposureResultStorage}.
     */
    @AutoValue
    public abstract static class Row {

        /**
         * Builder for {@link Row}.
         */
        @AutoValue.Builder
        public abstract static class Builder {

            public Row build() {
                Row row = autoBuild();
                byte[] signatureHash = row.signatureHash();
                if (signatureHash != null) {
                    Preconditions.checkState(signatureHash.length == 32, "Signature hash not Sha256 length.");
                }
                return row;
            }

            abstract Row autoBuild();

            public abstract Builder setKey(TemporaryExposureKey temporaryTracingKey);

            @Deprecated
            public abstract Builder setPackageName(String packageName);

            @Deprecated
            public abstract Builder setSignatureHash(byte[] signatureHash);

            @Deprecated
            public abstract Builder setToken(String token);

            public abstract Builder setTokenRoot(byte[] tokenRoot);

            public abstract Builder setExposureResult(ExposureResult exposureResult);
        }

        public static Builder builder() {
            return new AutoValue_ExposureResultStorage_Row.Builder();
        }

        /**
         * {@link TemporaryExposureKey} this {@link ExposureResult} was calculated for.
         */
        public abstract TemporaryExposureKey key();

        /**
         * Package that requested this row's {@link ExposureResult}.
         */
        @Deprecated
        @Nullable
        public abstract String packageName();

        /**
         * Package signature required to differentiate client apps.
         */
        @Deprecated
        @Nullable
        public abstract byte[] signatureHash();

        /**
         * Token this {@link ExposureResult} was provided with.
         */
        @Deprecated
        @Nullable
        abstract String token();

        @Nullable
        abstract byte[] tokenRoot();

        /**
         * {@link ExposureResult} calculated for.
         */
        abstract ExposureResult exposureResult();
    }

    /**
     * See {@link KeyEncoder} for details.
     */
    private static class PackageRootEncoder extends SerialEncoder {

        PackageRootEncoder(String packageName, byte[] signatureHash) {
            super(
                    ImmutableList.of(new Sha256StringEncoder(packageName), new BytesEncoder(signatureHash)));
        }
    }

    /**
     * See {@link KeyEncoder} for details.
     */
    private static class TokenRootEncoder extends SerialEncoder {

        TokenRootEncoder(String packageName, byte[] signatureHash, String token) {
            super(
                    ImmutableList.of(
                            new PackageRootEncoder(packageName, signatureHash), new Sha256StringEncoder(token)));
        }
    }

    /**
     * Encodes token root into byte array.
     */
    public static byte[] encodeTokenRoot(String packageName, byte[] signatureHash, String token) {
        return new ExposureResultStorage.TokenRootEncoder(packageName, signatureHash, token).encode();
    }

    /**
     * This {@link Encoder} is a four part encoding:
     *
     * <ol>
     *   <li>Sha256 hashed package name
     *   <li>Signature byte array (assumed to be Sha256)
     *   <li>Sha256 hashed token
     *   <li>Exposure key byte array ({@link ContactTracingFeature#contactIdLength()} in length)
     * </ol>
     * <p>
     * To encode only up to each of the parts, use the respective root encoders above:
     *
     * <ul>
     *   <li>To encode up to package and signature, use {@link PackageRootEncoder}
     *   <li>To encode up to token, use {@link TokenRootEncoder}
     * </ul>
     */
    private static class KeyEncoder extends SerialEncoder {

        KeyEncoder(byte[] tokenRoot, byte[] exposureKey) {
            super(ImmutableList.of(new BytesEncoder(tokenRoot), new BytesEncoder(exposureKey)));
        }

        /**
         * @deprecated use {@link #KeyEncoder(byte[], byte[])} instead.
         */
        @Deprecated
        KeyEncoder(String packageName, byte[] signatureHash, String token, byte[] exposureKey) {
            super(
                    ImmutableList.of(
                            new TokenRootEncoder(packageName, signatureHash, token),
                            new BytesEncoder(exposureKey)));
        }

        /**
         * @deprecated use {@link #KeyEncoder(byte[], byte[])} instead.
         */
        @Deprecated
        @SuppressWarnings("nullness")
        KeyEncoder(Row row) {
            this(row.packageName(), row.signatureHash(), row.token(), row.key().getKeyData());
        }
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

import androidx.annotation.NonNull;

import com.google.samples.exposurenotification.Log;
import com.google.samples.exposurenotification.nearby.TemporaryExposureKey;
import com.google.samples.exposurenotification.data.TemporaryExposureKeySupport;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * TODO: Implement a way to store {@link TemporaryExposureKey}s.
 */
public class SelfTemporaryExposureKeyDataStore {
    private List<TemporaryExposureKey> storedKeys = new ArrayList<>();

    public void putKey(TemporaryExposureKey temporaryExposureKey) {
        if (!storedKeys.contains(temporaryExposureKey)) {
            storedKeys.add(temporaryExposureKey);
        }
    }

    public boolean hasKey(TemporaryExposureKey temporaryExposureKey) {
        return storedKeys.contains(temporaryExposureKey);
    }

    public CloseableIterable<TemporaryExposureKey> getAllKeys() throws StorageException {
        return new CloseableIterable<TemporaryExposureKey>() {
            @Override
            public void close() throws IOException {
                // Nothing to close in this stub.
            }

            @NonNull
            @Override
            public Iterator<TemporaryExposureKey> iterator() {
                return storedKeys.iterator();
            }
        };
    }

    public TemporaryExposureKey getSingleKeyForIntervalNumber(int intervalNumber) {
        Log.log.atInfo().log("Getting exposure key that covers interval number=%d.", intervalNumber);
        try (CloseableIterable<TemporaryExposureKey> iterableStoredKeys = getAllKeys()) {
            for (TemporaryExposureKey key : iterableStoredKeys) {
                if (key.getRollingStartIntervalNumber() <= intervalNumber
                        && intervalNumber < TemporaryExposureKeySupport.getRollingEndIntervalNumber(key)) {
                    Log.log.atInfo().log("Exposure key found");
                    return key;
                }
            }
        } catch (StorageException | IOException e) {
            Log.log.atWarning().withCause(e).log("Exposure key not found.");
            return null;
        }
        Log.log.atInfo().log("Exposure key not found.");
        return null;
    }
}
/*
 * Copyright 2020 Google LLC
 *
 * 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 com.google.samples.exposurenotification.storage;

/** A storage exception in exposure notification. */
public class StorageException extends Exception {
    public StorageException(String msg) {
        super(msg);
    }

    public StorageException(Throwable cause) {
        super(cause);
    }

    public StorageException(String msg, Throwable cause) {
        super(msg, cause);
    }
}
package com.google.samples.exposurenotification.threads;

import android.os.Handler;

/**
 * This interface is used only to preserve compatibility with the annotations used in the Exposure
 * Notifications code, but the actual implementation is not available here because it's not relevant
 * to the usage of Exposure Notifications.
 */
public class TracingHandler extends Handler {
}
syntax = "proto2";

package com.google.samples.exposurenotification.storage;

import "exposure_key_export.proto";
import "exposure_result.proto";
import "matching_request.proto";

option java_multiple_files = true;
option java_package = "com.google.samples.exposurenotification.storage";

// the collection of all advertisement records.
// next index: 2
message ApiCallRecords {
  repeated ApiCallRecord records = 1;
}

// A record for one API call.
// next index: 13
message ApiCallRecord {
  // The package name of the client app.
  optional string package_name = 1;
  // Epoch timestamp in milliseconds of the API call.
  optional int64 timestamp_millis = 2;
  // The parameters of the API call. Refer to API definition.
  oneof method_call {
    StartMethod start = 3;
    StopMethod stop = 4;
    IsEnabledMethod is_enabled = 5;
    GetTemporaryExposureKeyHistoryMethod get_temporary_exposure_key_history = 6;
    ProvideDiagnosisKeysMethod provide_diagnosis_keys = 7;
    GetExposureSummaryMethod get_exposure_summary = 8;
    GetExposureInformationMethod get_exposure_information = 9;
    GetExposureWindowsMethod get_exposure_windows = 12;
  }
  // Status code of the API call.
  optional int32 status_code = 10;
  // Signature Hash of the calling package.
  optional bytes signature_hash = 11;
}

// start() method.
message StartMethod {
  // Human-readable application name.
  optional string app_name = 1;
}

// stop() method.
message StopMethod {}

// isEnabled() method
message IsEnabledMethod {
  // Return value of this method.
  optional bool return_value = 1;
}

// getTemporaryExposureKeyHistory() method.
message GetTemporaryExposureKeyHistoryMethod {
  // Return value of this method.
  repeated TemporaryExposureKey return_value = 1;
}

// Represents the information about a client provided diagnosis key file.
message DiagnosisKeyFile {
  // The size of the zip archive, in bytes.
  optional uint32 zip_file_size = 1;
  // Whether the zip contains signature.
  optional bool has_signature = 2;
  // Size of the key file after decompression, in bytes.
  optional uint32 key_file_size = 3;
}

// Some information of a list of key files provided by the client side.
message DiagnosisKeyFiles {
  repeated DiagnosisKeyFile key_files = 1;
  // A hash of the files in the current batch.
  optional bytes file_hash = 2;
}

// provideDiagnosisKeys() method.
message ProvideDiagnosisKeysMethod {
  // The paths of all the key files.
  optional DiagnosisKeyFiles key_files = 1;
  // Exposure configuration.
  optional ExposureConfigurationProto exposure_configuration = 2;
  // Unique token for this batch.
  optional string token = 3;
}

// Proto equivalence for
// com.google.samples.exposurenotification.ExposureSummary.
message ExposureSummaryProto {
  optional int32 days_since_last_exposure = 1;
  optional int32 matched_key_count = 2;
  optional int32 maximum_risk_score = 3;
  repeated int32 attenuation_durations = 4;
  optional int32 summation_risk_score = 5;
}

// getExposureSummary() method.
message GetExposureSummaryMethod {
  // Return value of this method.
  optional ExposureSummaryProto return_value = 1;
  // Unique token for this batch.
  optional string token = 2;
}

// Proto equivalence for
// com.google.samples.exposurenotification.ExposureInformation.
message ExposureInformationProto {
  optional int64 date_millis_since_epoch = 1;
  optional int32 duration_minutes = 2;
  optional int32 attenuation_value = 3;
  optional int32 transmission_risk_level = 4;
  optional int32 total_risk_score = 5;
  repeated int32 attenuation_durations = 6;
}

// getExposureInformation() method.
message GetExposureInformationMethod {
  // Return value of this method.
  repeated ExposureInformationProto return_value = 1;
  // Unique token for this batch.
  optional string token = 2;
}

// getExposureWindows() method.
message GetExposureWindowsMethod {
  // Return value of this method.
  repeated ExposureWindowProto return_value = 1;
  // Unique token for this batch.
  optional string token = 2;
}
syntax = "proto2";

package com.google.samples.exposurenotification.storage;

import "exposure_key_export.proto";
import "exposure_notification_enums.proto";

option java_multiple_files = true;
option java_package = "com.google.samples.exposurenotification.storage";

// the collection of all client records
// next index: 2
message ClientRecords {
  repeated ClientRecord client_records = 1;
}

// a record of all information for a single client
// next index: 11
message ClientRecord {
  // the client's android package name
  optional string package_name = 1;

  // sha256 hash of the client's package signature
  optional bytes signature_hash = 2;

  // whether the user has consented to tracking for exposure notifications
  // (which does not imply the user has consented to sharing their own
  // diagnosis), and is also not the same as whether exposure notification is
  // currently active for this client
  optional bool exposure_notification_consent = 3;

  // if set, the client has requested active exposure notifications
  optional ExposureNotificationRequest exposure_notification_request = 4;

  // the last time "Share Keys" dialog was accepted by user;
  // since v1.6+, controlled by flag, we require dialog every time (i.e.
  // disabled grace period), so is then treated as a boolean:
  // hasShareKeysDialogConsentedMillis: true - allow, false - show dialog.
  optional int64 share_keys_dialog_consented_millis = 5;

  // Last known timestamp for which matching was last performed for client
  optional int64 last_matching_timestamp_millis = 6;

  // client specific throttling state (for various APIs)
  optional ClientThrottling client_throttling = 7;

  // An archived list of completed matching request records for this client
  repeated CompletedMatchingRequestRecord completed_matching_request_records =
      8;
  // Last known timestamp for which the client requested active exposure
  // notifications
  optional int64 last_exposure_notification_request_set_timestamp_millis = 9;

  // configuration of TEKs for calculating risk score.
  optional TekMetadataRecord tek_metadata_record = 10;
}

// all data associated with a client's exposure notification request
// next index: 3
message ExposureNotificationRequest {
  // exposure configuration options
  // deprecated because exposure configuration is no longer in start() API.
  optional ExposureConfigurationInfo exposure_configuration = 1
  [deprecated = true];

  // when the request has been set for the client; means it's been active since
  // then
  optional int64 created_millis = 2;
}

// configuration options on how to determine if a client has been exposed. see
// documentation for ExposureConfiguration client class for more information.
// next index: 10
message ExposureConfigurationInfo {
  optional int32 minimum_risk_score = 1;

  repeated int32 bucketed_attenuation_scores = 2;

  optional int32 attenuation_weight = 3;

  repeated int32 bucketed_latency_days_scores = 4;

  optional int32 latency_days_weight = 5;

  repeated int32 bucketed_duration_scores = 6;

  optional int32 duration_weight = 7;

  repeated int32 bucketed_transmission_risk_scores = 8;

  optional int32 transmission_risk_weight = 9;
}

// holds all throttling representations for the given client
// next index: 4
message ClientThrottling {
  reserved 1;
  // provideDiagnosisKeys 24h throttling state
  optional ThrottleState client_provide_diagnosis_keys_24_hour_throttle = 2;

  // setDiagnosisKeysDataMapping throttling state
  optional ThrottleState set_diagnosis_keys_data_mapping_throttle = 3;
}

// simple fixed window throttling state
// next index: 5
message ThrottleState {
  reserved 3;

  // latest window start time
  optional int64 window_start = 1;

  // unthrottled access count in latest window
  optional int32 window_count = 2;

  // unthrottled access with TOKEN_A count in latest window
  optional int32 window_count_with_token_a = 4;
}

// A record representing a completed matching request and the exposure result
// summary next index: 5
message CompletedMatchingRequestRecord {
  // The time in milliseconds since epoch indicating the request completion time
  optional int64 timestamp_millis = 1 [default = 0];
  // The number of keys involved in this request's matching process
  optional int32 key_count = 2;
  // The number of matched key counts found in this request
  optional int32 matches_count = 3;
  // Composite sha256 hash of the temporary exposure keys passed for this
  // request
  optional string temporary_keys_batch_hash = 4;
}

// holds configuration from setDiagnosisKeysDataMapping API call.
// next index: 4
message TekMetadataRecord {
  // Mapping from diagnosisKey.daysSinceOnset to Infectiousness.
  repeated Infectiousness days_since_onset_to_infectiousness = 1;

  // Report type to default to when a TEK has no report type.
  optional TemporaryExposureKey.ReportType report_type_when_missing = 2
  [default = CONFIRMED_TEST];

  // Returns the Infectiousness for TEKs which don't contain onset day.
  optional Infectiousness infectiousness_when_days_since_onset_missing = 3
  [default = INFECTIOUSNESS_STANDARD];
}
syntax = "proto2";

package com.google.samples.exposurenotification.storage;

option java_multiple_files = true;
option java_package = "com.google.samples.exposurenotification.storage";

// The record value of a sighted device in a 10 to 20 minutes window. One
// contact record corresponds to one Rolling Proximity Id. Contact record
// contains one or more `SightingRecord`s representing detailed signals from BLE
// sightings.
message ContactRecordValue {
  repeated SightingRecord sighting_records = 1;

  // Encrypted metadata received from the remote device that can only be
  // decrypted with the right encryption keys derived from diagnosis key.
  //
  // This information is received in each received packet, but the encrypted
  // metadata should be unchanged during the same rolling proximity ID window.
  // Thus we store it here to compact the storage. However we do allow
  // individual packet to have different metadata, in such case we'll set
  // `SightingRecord.encrypted_metadata` which overtakes this value.
  optional bytes encrypted_metadata = 2;
}

// The record of one BLE sighting. A sighting is defined as: at least one packet
// is received from the remote device within a scan cycle which has a duration
// of 4 minutes. Multiple packets received are grouped as one sighting record.
// Next tag: 7
message SightingRecord {
  // The choice of uint32: 1) to save 4 bytes each record comparing to (u)int64,
  // 2) granularity finer than seconds is not necessary. 3) MAX(int32) since
  // epoch is not big enough so an uint is chose.
  optional uint32 epoch_seconds = 1;

  // The Received signal strength indicator of all raw BLE packets received of
  // this sighting. Each byte in this array represent one RSSI value of a
  // packet.
  //
  // This field is essentially "repeated int8 rssi_values". A valid sighting
  // record should have a minimum size of 1.
  optional bytes rssi_values = 2;

  // Deprecated: use rssi_values.
  optional int32 rssi = 3 [deprecated = true];

  // Encrypted metadata received from the remote device that can only be
  // decrypted with the right encryption keys derived from diagnosis key.
  //
  // This field shall not be set unless the AEM is changed from the beginning
  // of the rolling proximity id validation window. When this field is
  // unset, use `encrypted_metadata` in `ContactRecordValue`.
  //
  // When set, this repeated field will have a number of elements that is equal
  // to the length of `rssi_values`. Each element is a 4-byte array per the
  // definition of Associated Encrypted Metadata.
  repeated bytes encrypted_metadata = 6;

  // Deprecated: use encrypted_metadata.
  optional bytes associated_encrypted_metadata = 4 [deprecated = true];

  // Timestamp of the previous scan instance (potentially empty or not
  // containing this RPI). Scan in this context is either explicitly requested
  // (via start/stop calls) or the last result of opportunistic scanning. If
  // there is no previous scan recorded (e.g., due to device reboot or service
  // crashing), defaults to 0.
  optional uint32 previous_scan_epoch_seconds = 5;
}
/*
 *
 * NOTE:  This proto is agreed upon with Apple as well as servers that
 *        generate EN Exposure Key export files. Do not make non-essential
 *        changes or non-backwards compatible edits.
 *
 * See:
 * https://github.com/google/exposure-notifications-server/blob/main/internal/pb/export/export.proto
 * https://developers.google.com/android/exposure-notifications/exposure-key-file-format
 * https://developer.apple.com/documentation/exposurenotification/setting_up_an_exposure_notification_server#3591262
 * https://github.com/google/exposure-notifications-android/blob/master/app/src/main/java/com/google/android/apps/exposurenotification/proto/key_file.proto
 *
 */
syntax = "proto2";  // Apple's framework proto libraries only support proto2

package com.google.samples.exposurenotification;

import "nanopb.proto";

option java_package = "com.google.samples.exposurenotification";
option java_outer_classname = "ExposureKeyExportProto";

// Protobuf definition for exports of confirmed temporary exposure keys.
//
// The full file format is documented under "Exposure Key Export File Format
// and Verification" at https://www.google.com/covid19/exposurenotifications/
//
// These files have a 16-byte, space-padded header before the protobuf data
// starts. They will be contained in a zip archive, alongside a signature
// file verifying the contents.
message TemporaryExposureKeyExport {
  // Time window of keys in this file based on arrival to server, in UTC
  // seconds
  optional fixed64 start_timestamp = 1;
  optional fixed64 end_timestamp = 2;

  // Region for which these keys came from (e.g., country)
  optional string region = 3;

  // E.g., Batch 2 of 10. Ordinal, 1-based numbering.
  // Note: Not yet supported on iOS. Use values of 1 for both.
  optional int32 batch_num = 4;
  optional int32 batch_size = 5;

  // Information about signatures
  repeated SignatureInfo signature_infos = 6;

  // The TemporaryExposureKeys for initial release of keys.
  // Keys should be included in this list for initial release,
  // whereas revised or revoked keys should go in revised_keys.
  repeated TemporaryExposureKey keys = 7;

  // TemporaryExposureKeys that have changed status.
  // Keys should be included in this list if they have changed status
  // or have been revoked.
  repeated TemporaryExposureKey revised_keys = 8;
}

message SignatureInfo {
  // These fields are no longer needed (confirmed with Apple)
  // Apple read bundle id from an app's metadata, and we were
  // always using calling app package. It's not needed in the file.
  reserved 1, 2;
  reserved "app_bundle_id", "android_package";
  // Key version for rollovers
  // Must be in character class [a-zA-Z0-9_]. E.g., 'v1'
  optional string verification_key_version = 3;
  // Alias with which to identify public key to be used for verification
  // Must be in character class [a-zA-Z0-9_]
  // For cross-compatibility with Apple, use MCC
  // (https://en.wikipedia.org/wiki/Mobile_country_code).
  optional string verification_key_id = 4;
  // ASN.1 OID for Algorithm Identifier. Supported algorithms are
  // either 1.2.840.10045.4.3.2 or 1.2.840.10045.4.3.4
  optional string signature_algorithm = 5;
}

message TemporaryExposureKey {
  // Key of infected user
  optional bytes key_data = 1
  [(nanopb).type = FT_STATIC, (nanopb).max_size = 16];

  // Varying risks associated with exposure depending on type of verification
  // Ignored by the v1.5 API when report_type is set.
  optional int32 transmission_risk_level = 2 [deprecated = true];

  // The interval number since epoch for which a key starts
  optional int32 rolling_start_interval_number = 3;

  // Increments of 10 minutes describing how long a key is valid
  optional int32 rolling_period = 4 [default = 144];  // defaults to 24 hours

  // Data type representing why this key was published.
  enum ReportType {
    // Never returned by the API.
    REPORT_TYPE_UNKNOWN = 0;
    CONFIRMED_TEST = 1;
    CONFIRMED_CLINICAL_DIAGNOSIS = 2;
    SELF_REPORT = 3;
    // Reserved for future use.
    RECURSIVE = 4;
    // Used to revoke a key, never returned by API.
    REVOKED = 5;
  }

  // Type of diagnosis associated with a key.
  optional ReportType report_type = 5;

  // Number of days elapsed between symptom onset and the TEK being used.
  // E.g. 2 means TEK is 2 days after onset of symptoms.
  optional sint32 days_since_onset_of_symptoms = 6;
}

message TEKSignatureList {
  repeated TEKSignature signatures = 1;
}

message TEKSignature {
  // Info about the signing key, version, algorithm, etc. Only the
  // verification_key_id, verification_key_version, and
  // signature_algorithm fields within signature_info are read.
  optional SignatureInfo signature_info = 1;
  // E.g., Batch 2 of 10 - these fields are ignored on android in favor of the
  // batch fields within TemporaryExposureKeyExport
  optional int32 batch_num = 2;
  optional int32 batch_size = 3;
  // Signature in X9.62 format (ASN.1 SEQUENCE of two INTEGER fields)
  optional bytes signature = 4;
}
syntax = "proto2";  // Apple's framework proto libraries only support proto2

package com.google.samples.exposurenotification;

option java_package = "com.google.samples.exposurenotification";
option java_outer_classname = "ExposureNotificationEnums";

// The confidence value indicates where the calibration data comes from.
// The higher value means the higher confidence.
enum CalibrationConfidence {
  // No calibration data, using fleet-wide as default options.
  LOWEST_CONFIDENCE = 0;

  // Using average calibration over models from manufacturer.
  LOW_CONFIDENCE = 1;

  // Using single-antenna orientation for a similar model.
  MEDIUM_CONFIDENCE = 2;

  // Using significant calibration data for this model.
  HIGH_CONFIDENCE = 3;
}

// Infectiousness of the TEK that caused this exposure, computed from the days
// since onset of symptoms using the daysToInfectiousnessMapping.
enum Infectiousness {
  INFECTIOUSNESS_NONE = 0;
  INFECTIOUSNESS_STANDARD = 1;
  INFECTIOUSNESS_HIGH = 2;
}
syntax = "proto2";

package com.google.samples.exposurenotification.storage;

import "exposure_key_export.proto";
import "exposure_notification_enums.proto";
import "tracing_params_record.proto";

option java_multiple_files = true;
option java_package = "com.google.samples.exposurenotification.storage";

// A list of exposure matches found.
message ExposureResult {
  repeated ExposureRecord exposure_records = 1;
  optional TracingParamsRecord tracing_params_record = 2;

  // Day-level resolution that the exposure results were calculated for in
  // millis since epoch.
  optional int64 date_millis_since_epoch = 3;

  // Max risk score for these exposure records.
  optional int32 max_risk_score = 4;

  // Total score for these exposure records.
  optional int32 total_risk_score = 5;

  // Attenuation durations below, between, and above the thresholds specified in
  // the configuration as durationAtAttenuationThresholds, summed over all the
  // records in this result.
  repeated int32 attenuation_durations = 6;

  // All ExposureWindows attributed to the diagnosis key to which this
  // ExposureResult belongs.
  repeated ExposureWindowProto exposure_windows = 7;

  // Metadata related to the Temporary Exposure Key.
  optional TekMetadata tek_metadata = 8;

  // Number of tek_metadata#report_type is updated.
  optional int32 report_type_transition_count = 9;
}

// Match results based on the provided tracing parameters. Directly maps to
// the information passed back to the client when an exposure match is
// calculated. An instance of this object represents what the system deems to
// be an exposure based on the tracing parameters provided.
message ExposureRecord {
  reserved 1;
  // Length of exposure in 5 minute increments.
  optional int64 duration_seconds = 2;

  // Signal strength attenuation, representing the weighted average of devices
  // proximity through the exposure. This value is the advertiser's TX power
  // minus the receiver's RSSI.
  // The average is computed using time spent at attenuation levels as weights.
  // The attenuation level between two scans is a simple average of measured
  // attenuations. The first and last scans get extended by half of
  // scanInterval. Consider:
  //
  // scanInterval = 6
  // t=00, attenuation=1
  // t=10, attenuation=4
  // t=50, attenuation=5
  //
  // The resulting attenuation value is:
  //
  // (6/2 * 1 + (10 - 0) * 1 + (50 - 10) * 4 + 6/2 * 5) /
  //                                         (3 + 10 + 40 +3) = 188 / 56 = 3.36
  //
  // The result is rounded to 3.
  //
  // Note: This value may be misleading, higher attenuation does not necessarily
  // mean farther away. Phone in pocket vs hand can greatly affect this value,
  // along with other situations that can block the signal.
  //
  // This value will be in the range 0-255.
  optional int32 attenuation_value = 3;

  // Risk score for this record.
  optional int32 risk_score = 4;

  // Attenuation durations below, between, and above the thresholds specified in
  // the configuration as durationAtAttenuationThresholds.
  repeated int32 attenuation_durations = 5;

  // Transmission risk level associated with this record.
  optional int32 transmission_risk_level = 6;
}

// Internal representation, as described in go/exposure-windows.
message ExposureWindowProto {
  // Exact time in seconds assigned for this exposure window.
  optional int32 epoch_seconds = 1;

  // All scans (potentially containing multiple sightings) in this window.
  repeated ScanInstanceProto scan_instances = 2;

  // Metadata related to the Temporary Exposure Key.
  optional TekMetadata tek_metadata = 3;
}

// A single instance of BLE scan, potentially containing multiple beacon
// sightings.
message ScanInstanceProto {
  // A list of all attenuations of this TEK in this scan.
  repeated int32 attenuations = 1;

  // Since the previous scan, regardless if it contained this TEK or any
  // sightings at all.
  optional int32 seconds_since_last_scan = 2;
}

message TekMetadata {
  reserved 2;

  // Type of diagnosis associated with a key.
  optional TemporaryExposureKey.ReportType report_type = 1;

  // Infectiousness of diagnosis associated with a key.
  optional Infectiousness infectiousness = 3;

  // Calibration confidence of diagnosis associated with a key.
  optional CalibrationConfidence calibration_confidence = 4;
}

// Internal representation, as described in go/exposure-windows.
message DailySummariesProto {
  optional int32 days_since_epoch = 1;

  optional ExposureSummaryDataProto confirmed_test_summary = 2;

  optional ExposureSummaryDataProto confirmed_clinical_summary = 3;

  optional ExposureSummaryDataProto recursive_summary = 4;

  optional ExposureSummaryDataProto self_report_summary = 5;

  optional ExposureSummaryDataProto day_summary = 6;
}

// Internal representation, as described in go/exposure-windows.
message ExposureSummaryDataProto {
  optional double maximum_score = 1;

  optional double score_sum = 2;

  optional double weighted_duration_sum = 3;
}
syntax = "proto2";

package com.google.samples.exposurenotification.storage;

option java_multiple_files = true;
option java_package = "com.google.samples.exposurenotification.storage";

// This message is used to store all information about a matching request. It is
// called MatchingRequest*Proto* to avoid name conflict with the Java class
// MatchingRequest for public surfacing.
message MatchingRequestProto {
  // The package name the feed is from.
  optional string package_name = 1;

  // The signature of the client app, together with package_name to uniquely
  // identify a client.
  optional bytes signature_hash = 2;

  // The client provided string for result retrieval.
  optional string token = 3;

  // The exposure configuration in proto format.
  optional ExposureConfigurationProto exposure_configuration = 4;

  // A list of file paths that the diagnosis keys are in.
  repeated string key_files = 5;

  // The SHA-256 hash of all key files in the matching request, in base64.
  optional string key_files_hash = 6;

  // The time in milliseconds this request was received.
  optional int64 request_time_millis = 7;
}

// A proto representation of
// com.google.samples.exposurenotification.nearby.ExposureConfiguration.
message ExposureConfigurationProto {
  optional int32 minimum_risk_score = 1;

  repeated int32 attenuation_scores = 2;
  optional int32 attenuation_weight = 3;

  repeated int32 days_since_last_exposure_scores = 4;
  optional int32 days_since_last_exposure_weight = 5;

  repeated int32 duration_scores = 6;
  optional int32 duration_weight = 7;

  repeated int32 transmission_risk_scores = 8;
  optional int32 transmission_risk_weight = 9;

  repeated int32 duration_at_attenuation_thresholds = 10;
}
syntax = "proto2";

package com.google.samples.exposurenotification.storage;

option java_multiple_files = true;
option java_package = "com.google.samples.exposurenotification.storage";

// Metadata corresponding to a temporary exposure key.
message TemporaryExposureKeyProperties {
  // The number of intervals that the key is valid for from its start interval.
  optional int32 rolling_period = 1 [default = 144];

  // The starting interval number for the given temporary exposure key
  optional int32 rolling_start_interval_number = 2;
}
syntax = "proto2";

package com.google.samples.exposurenotification.storage;

option java_multiple_files = true;
option java_package = "com.google.samples.exposurenotification.storage";

// The parameters used when tracing bluetooth signals for exposure.
message TracingParamsRecord {
  // Minimum consecutive sightings in duration required to be considered a
  // contact, compared with a bucketized duration calculation.
  optional int64 min_exposure_bucketized_duration_seconds = 1;
  // Maximum duration gap between two sightings to be considered continuous
  // contact with another device.
  optional int64 max_interpolation_duration_seconds = 2;
  // Minimum ble signal strength indicator value that must be reached by at
  // least one sighting in a series of sightings that are being considered for
  // an exposure
  optional int32 min_attenuation_value = 3;
  // Bluetooth token scanning interval.
  optional int32 scan_interval_seconds = 4;
  // Whether to interpolate attenuation when calculating duration at thresholds.
  optional bool interpolation_enabled = 5;
}
#
# Copyright 2020 Google LLC
#
# 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.
#

# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
#
# Copyright 2020 Google LLC
#
# 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.
#

#Mon Jun 15 11:27:16 PDT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
#!/usr/bin/env sh

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn () {
    echo "$*"
}

die () {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
  NONSTOP* )
    nonstop=true
    ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=$((i+1))
    done
    case $i in
        (0) set -- ;;
        (1) set -- "$args0" ;;
        (2) set -- "$args0" "$args1" ;;
        (3) set -- "$args0" "$args1" "$args2" ;;
        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Escape application args
save () {
    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
    echo " "
}
APP_ARGS=$(save "$@")

# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"

# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
  cd "$(dirname "$0")"
fi

exec "$JAVACMD" "$@"
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto init

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:init
@rem Get command-line arguments, handling Windows variants

if not "%OS%" == "Windows_NT" goto win9xME_args

:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2

:win9xME_args_slurp
if "x%~1" == "x" goto execute

set CMD_LINE_ARGS=%*

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega
/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

include ':exposurenotification'
include ':app'
rootProject.name = "Exposure Notifications Snippets"
armeabi-v7a/
arm64-v8a/
x86/
x86_64/
src/
armeabi-v7a/
arm64-v8a/
x86/
x86_64/
src/
# Copyright 2020 Google LLC
#
# 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.
#
FROM golang

# Library versions
ARG BORINGSSL_COMMIT_ID
ENV ANDROID_NDK_VERSION r21d

# Install dependencies
RUN apt-get update && apt-get install --no-install-recommends -yq \
    ca-certificates cmake git unzip ninja-build && \
    rm -rf /var/lib/apt/lists/*

# Download the Android NDK
ENV ANDROID_NDK_ROOT /go/android-ndk-${ANDROID_NDK_VERSION}
RUN wget -qq https://dl.google.com/android/repository/android-ndk-${ANDROID_NDK_VERSION}-linux-x86_64.zip -O /go/android-ndk.zip && \
    unzip -qq /go/android-ndk.zip -d /go && \
    rm /go/android-ndk.zip

# Clone the BoringSSL source
ENV BORINGSSL_SRC /src/boringssl
RUN git clone https://boringssl.googlesource.com/boringssl ${BORINGSSL_SRC}

# Check out the appropriate commit for reproducibility
WORKDIR ${BORINGSSL_SRC}
RUN git checkout ${BORINGSSL_COMMIT_ID}

# Image arguments used to determine the target Android ABI
ARG ANDROID_ABI
RUN echo "Building BoringSSL for Android ${ANDROID_ABI}"

# Cross compile the Android binaries
WORKDIR /opt/boringssl/android/${ANDROID_ABI}
RUN cmake \
        -DANDROID_ABI=${ANDROID_ABI} \
        -DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK_ROOT}/build/cmake/android.toolchain.cmake \
        -DANDROID_NATIVE_API_LEVEL=16 \
        -GNinja ${BORINGSSL_SRC} && \
    ninja all

#!/bin/sh
# Copyright 2020 Google LLC
#
# 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.
#

readonly BORINGSSL_IMAGE="boringssl"
readonly BORINGSSL_COMMIT_ID="bc2480510960a77bea24edc64fcb089aca103940"

#######################################
# Cross-compiles BoringSSL for Android using the Docker image in this folder
# Globals:
#   BORINGSSL_IMAGE: Name of the docker image used to build BoringSSL
# Arguments:
#   android_abi: ABI flavour, e.g. armeabi-v7a.
#######################################
boringssl_android_build() {

  # Process arguments
  local -r android_abi=$1

  # Build the Docker image for the specific Android ABI
  local -r dockerfile_path=`dirname "$0"`
  docker build --rm \
    --build-arg ANDROID_ABI=$android_abi \
    --build-arg BORINGSSL_COMMIT_ID=$BORINGSSL_COMMIT_ID \
    --tag $BORINGSSL_IMAGE:$android_abi \
    $dockerfile_path
}

#######################################
# Copies the build artifacts for the given Android ABI flavour into the
# provided destination folder.
# Globals:
#   BORINGSSL_IMAGE: Name of the docker image used to build BoringSSL
# Arguments:
#   android_abi: ABI flavour, e.g. armeabi-v7a.
#   dest: Destination folder to place artifacts.
#######################################
copy_build_artifacts() {

  # Process arguments
  local -r android_abi=$1
  local -r dest=$2

  # Create a Docker image and tar the build artifacts into local storage
  local -r image_id=$(docker create ${BORINGSSL_IMAGE}:${android_abi})
  local -r output_tar="/tmp/boringssl-${android_abi}.tar"
  docker cp ${image_id}:"/opt/boringssl/android/${android_abi}" - > $output_tar
  docker rm -v $image_id

  # Extract the tar into the desired destination folder
  mkdir -p $dest
  tar -xvf $output_tar -C $dest
}

# Determine the location of the base directory
readonly BASE_DIR=$(dirname "$0")

# Download the source so we can reference the header files
git clone https://boringssl.googlesource.com/boringssl "$BASE_DIR"/src
git --git-dir "$BASE_DIR"/src/.git checkout ${BORINGSSL_COMMIT_ID}

# Compile each ABI and output artifacts into a subfolder
for flavour in "armeabi-v7a" "arm64-v8a" "x86" "x86_64"
do
    boringssl_android_build $flavour
    copy_build_artifacts $flavour "$BASE_DIR"
done
linux/
macosx/
windows/
#!/bin/sh
# Copyright 2020 Google LLC
#
# 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.
#

#######################################
# Download and extract the nanopb precompiled binaries for the given platform
# Arguments:
#   dest: destination folder for the binaries
#   platform: linux, macosx or windows
#######################################
download_nanopb() {

  # Process arguments
  local -r dest=$1
  local -r platform=$2

  # Download appropriate binaries
  local -r output="${dest}/${platform}"
  local -r tar_file="${dest}/${platform}.tar.gz"
  wget -c "https://jpa.kapsi.fi/nanopb/download/nanopb-0.4.2-${platform}-x86.tar.gz" -O "$tar_file"
  mkdir -p "$output" && tar xvf "$tar_file" -C "$output" --strip-components 1 && rm -rf "$tar_file"
}

readonly base_path=`dirname "$0"`
download_nanopb "$base_path" linux
download_nanopb "$base_path" macosx