Add differential privacy library and algorithms

- Created a differential privacy framework interface
- Added 2 DP algorithms in DP framework:
  -  Rappor, a wrapper based on external/rappor project
  -  Longitudinal Reporting, DP enhancement based on Rappor
- Created Privacy Tests for testing all privacy libraries
- Added original Rappor test case in privacy test
- Created tests to verify Rappor and Longitudinal Reporting result in DP framework

Test: bit FrameworksPrivacyLibraryTests:android.privacy.LongitudinalReportingEncoderTest
Test: bit FrameworksPrivacyLibraryTests:android.privacy.RapporEncoderTest

Change-Id: Id460665059653924434c141686b5cad3fb697046
diff --git a/Android.bp b/Android.bp
index 979ed1c..80eaa7d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -671,6 +671,7 @@
         "libphonenumber-platform",
         "nist-sip",
         "tagsoup",
+        "rappor",
     ],
     dxflags: ["--core-library"],
 }
diff --git a/core/java/android/privacy/DifferentialPrivacyConfig.java b/core/java/android/privacy/DifferentialPrivacyConfig.java
new file mode 100644
index 0000000..e14893e
--- /dev/null
+++ b/core/java/android/privacy/DifferentialPrivacyConfig.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package android.privacy;
+
+/**
+ * An interface for differential privacy configuration.
+ * {@link DifferentialPrivacyEncoder} will apply this configuration to do differential privacy
+ * encoding.
+ *
+ * @hide
+ */
+public interface DifferentialPrivacyConfig {
+
+    /**
+     * Returns the name of the algorithm used in differential privacy config.
+     *
+     * @return The name of the algorithm
+     */
+    String getAlgorithm();
+}
diff --git a/core/java/android/privacy/DifferentialPrivacyEncoder.java b/core/java/android/privacy/DifferentialPrivacyEncoder.java
new file mode 100644
index 0000000..9355d6a
--- /dev/null
+++ b/core/java/android/privacy/DifferentialPrivacyEncoder.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package android.privacy;
+
+/**
+ * An interface for differential privacy encoder.
+ * Applications can use it to convert privacy sensitive data to privacy protected report.
+ * There is no decoder implemented in Android as it is not possible decode a single report by
+ * design.
+ *
+ * <p>Each type of log should have its own encoder, otherwise it may leak
+ * some information about Permanent Randomized Response(PRR, is used to create a “noisy”
+ * answer which is memoized by the client and permanently reused in place of the real answer).
+ *
+ * <p>Some encoders may not support all encoding methods, and it will throw {@link
+ * UnsupportedOperationException} if you call unsupported encoding method.
+ *
+ * <p><b>WARNING:</b> Privacy protection works only when encoder uses a suitable DP configuration,
+ * and the configuration and algorithm that is suitable is highly dependent on the use case.
+ * If the configuration is not suitable for the use case, it may hurt privacy or utility or both.
+ *
+ * @hide
+ */
+public interface DifferentialPrivacyEncoder {
+
+    /**
+     * Apply differential privacy to encode a string.
+     *
+     * @param original An arbitrary string
+     * @return Differential privacy encoded bytes derived from the string
+     */
+    byte[] encodeString(String original);
+
+    /**
+     * Apply differential privacy to encode a boolean.
+     *
+     * @param original An arbitrary boolean.
+     * @return Differential privacy encoded bytes derived from the boolean
+     */
+    byte[] encodeBoolean(boolean original);
+
+    /**
+     * Apply differential privacy to encode sequence of bytes.
+     *
+     * @param original An arbitrary byte array.
+     * @return Differential privacy encoded bytes derived from the bytes
+     */
+    byte[] encodeBits(byte[] original);
+
+    /**
+     * Returns the configuration that this encoder is using.
+     */
+    DifferentialPrivacyConfig getConfig();
+
+    /**
+     * Return True if the output from encoder is NOT securely randomized, otherwise encoder should
+     * be secure to randomize input.
+     *
+     * <b> A non-secure encoder is intended only for testing only and must not be used to process
+     * real data.
+     * </b>
+     */
+    boolean isInsecureEncoderForTest();
+}
diff --git a/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java b/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java
new file mode 100644
index 0000000..ee910fc
--- /dev/null
+++ b/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingConfig.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package android.privacy.internal.longitudinalreporting;
+
+import android.privacy.DifferentialPrivacyConfig;
+import android.privacy.internal.rappor.RapporConfig;
+import android.text.TextUtils;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * A class to store {@link LongitudinalReportingEncoder} configuration.
+ *
+ * <ul>
+ * <li> f is probability to flip input value, used in IRR.
+ * <li> p is probability to override input value, used in PRR1.
+ * <li> q is probability to set input value as 1 when result of PRR(p) is true, used in PRR2.
+ * </ul>
+ *
+ * @hide
+ */
+public class LongitudinalReportingConfig implements DifferentialPrivacyConfig {
+
+    private static final String ALGORITHM_NAME = "LongitudinalReporting";
+
+    // Probability to flip input value.
+    private final double mProbabilityF;
+
+    // Probability to override original value.
+    private final double mProbabilityP;
+    // Probability to override value with 1.
+    private final double mProbabilityQ;
+
+    // IRR config to randomize original value
+    private final RapporConfig mIRRConfig;
+
+    private final String mEncoderId;
+
+    /**
+     * Constructor to create {@link LongitudinalReportingConfig} used for {@link
+     * LongitudinalReportingEncoder}
+     *
+     * @param encoderId    Unique encoder id.
+     * @param probabilityF Probability F used in Longitudinal Reporting algorithm.
+     * @param probabilityP Probability P used in Longitudinal Reporting algorithm. This will be
+     *                     quantized to the nearest 1/256.
+     * @param probabilityQ Probability Q used in Longitudinal Reporting algorithm. This will be
+     *                     quantized to the nearest 1/256.
+     */
+    public LongitudinalReportingConfig(String encoderId, double probabilityF,
+            double probabilityP, double probabilityQ) {
+        Preconditions.checkArgument(probabilityF >= 0 && probabilityF <= 1,
+                "probabilityF must be in range [0.0, 1.0]");
+        this.mProbabilityF = probabilityF;
+        Preconditions.checkArgument(probabilityP >= 0 && probabilityP <= 1,
+                "probabilityP must be in range [0.0, 1.0]");
+        this.mProbabilityP = probabilityP;
+        Preconditions.checkArgument(probabilityQ >= 0 && probabilityQ <= 1,
+                "probabilityQ must be in range [0.0, 1.0]");
+        this.mProbabilityQ = probabilityQ;
+        Preconditions.checkArgument(!TextUtils.isEmpty(encoderId), "encoderId cannot be empty");
+        mEncoderId = encoderId;
+        mIRRConfig = new RapporConfig(encoderId, 1, 0.0, probabilityF, 1 - probabilityF, 1, 1);
+    }
+
+    @Override
+    public String getAlgorithm() {
+        return ALGORITHM_NAME;
+    }
+
+    RapporConfig getIRRConfig() {
+        return mIRRConfig;
+    }
+
+    double getProbabilityP() {
+        return mProbabilityP;
+    }
+
+    double getProbabilityQ() {
+        return mProbabilityQ;
+    }
+
+    String getEncoderId() {
+        return mEncoderId;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("EncoderId: %s, ProbabilityF: %.3f, ProbabilityP: %.3f"
+                        + ", ProbabilityQ: %.3f",
+                mEncoderId, mProbabilityF, mProbabilityP, mProbabilityQ);
+    }
+}
diff --git a/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java b/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java
new file mode 100644
index 0000000..219868d
--- /dev/null
+++ b/core/java/android/privacy/internal/longitudinalreporting/LongitudinalReportingEncoder.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package android.privacy.internal.longitudinalreporting;
+
+import android.privacy.DifferentialPrivacyEncoder;
+import android.privacy.internal.rappor.RapporConfig;
+import android.privacy.internal.rappor.RapporEncoder;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Differential privacy encoder by using Longitudinal Reporting algorithm.
+ *
+ * <b>
+ * Notes: It supports encodeBoolean() only for now.
+ * </b>
+ *
+ * <p>
+ * Definition:
+ * PRR = Permanent Randomized Response
+ * IRR = Instantaneous Randomized response
+ *
+ * Algorithm:
+ * Step 1: Create long-term secrets x(ignoreOriginalInput)=Ber(P), y=Ber(Q), where Ber denotes
+ * Bernoulli distribution on {0, 1}, and we use it as a long-term secret, we implement Ber(x) by
+ * using PRR(2x, 0) when x < 1/2, PRR(2(1-x), 1) when x >= 1/2.
+ * Step 2: If x is 0, report IRR(F, original), otherwise report IRR(F, y)
+ * </p>
+ *
+ * Reference: go/bit-reporting-with-longitudinal-privacy
+ * TODO: Add a public blog / site to explain how it works.
+ *
+ * @hide
+ */
+public class LongitudinalReportingEncoder implements DifferentialPrivacyEncoder {
+
+    // Suffix that will be added to Rappor's encoder id. There's a (relatively) small risk some
+    // other Rappor encoder may re-use the same encoder id.
+    private static final String PRR1_ENCODER_ID = "prr1_encoder_id";
+    private static final String PRR2_ENCODER_ID = "prr2_encoder_id";
+
+    private final LongitudinalReportingConfig mConfig;
+
+    // IRR encoder to encode input value.
+    private final RapporEncoder mIRREncoder;
+
+    // A value that used to replace original value as input, so there's always a chance we are
+    // doing IRR on a fake value not actual original value.
+    // Null if original value does not need to be replaced.
+    private final Boolean mFakeValue;
+
+    // True if encoder is securely randomized.
+    private final boolean mIsSecure;
+
+    /**
+     * Create {@link LongitudinalReportingEncoder} with
+     * {@link LongitudinalReportingConfig} provided.
+     *
+     * @param config     Longitudinal Reporting parameters to encode input
+     * @param userSecret User generated secret that used to generate PRR
+     * @return {@link LongitudinalReportingEncoder} instance
+     */
+    public static LongitudinalReportingEncoder createEncoder(LongitudinalReportingConfig config,
+            byte[] userSecret) {
+        return new LongitudinalReportingEncoder(config, true, userSecret);
+    }
+
+    /**
+     * Create <strong>insecure</strong> {@link LongitudinalReportingEncoder} with
+     * {@link LongitudinalReportingConfig} provided.
+     * Should not use it to process sensitive data.
+     *
+     * @param config Rappor parameters to encode input.
+     * @return {@link LongitudinalReportingEncoder} instance.
+     */
+    @VisibleForTesting
+    public static LongitudinalReportingEncoder createInsecureEncoderForTest(
+            LongitudinalReportingConfig config) {
+        return new LongitudinalReportingEncoder(config, false, null);
+    }
+
+    private LongitudinalReportingEncoder(LongitudinalReportingConfig config,
+            boolean secureEncoder, byte[] userSecret) {
+        mConfig = config;
+        mIsSecure = secureEncoder;
+        final boolean ignoreOriginalInput = getLongTermRandomizedResult(config.getProbabilityP(),
+                secureEncoder, userSecret, config.getEncoderId() + PRR1_ENCODER_ID);
+
+        if (ignoreOriginalInput) {
+            mFakeValue = getLongTermRandomizedResult(config.getProbabilityQ(),
+                    secureEncoder, userSecret, config.getEncoderId() + PRR2_ENCODER_ID);
+        } else {
+            // Not using fake value, so IRR will be processed on real input value.
+            mFakeValue = null;
+        }
+
+        final RapporConfig irrConfig = config.getIRRConfig();
+        mIRREncoder = secureEncoder
+                ? RapporEncoder.createEncoder(irrConfig, userSecret)
+                : RapporEncoder.createInsecureEncoderForTest(irrConfig);
+    }
+
+    @Override
+    public byte[] encodeString(String original) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public byte[] encodeBoolean(boolean original) {
+        if (mFakeValue != null) {
+            // Use the fake value generated in PRR.
+            original = mFakeValue.booleanValue();
+        }
+        return mIRREncoder.encodeBoolean(original);
+    }
+
+    @Override
+    public byte[] encodeBits(byte[] bits) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public LongitudinalReportingConfig getConfig() {
+        return mConfig;
+    }
+
+    @Override
+    public boolean isInsecureEncoderForTest() {
+        return !mIsSecure;
+    }
+
+    /**
+     * Get PRR result that with probability p is 1, probability 1-p is 0.
+     */
+    @VisibleForTesting
+    public static boolean getLongTermRandomizedResult(double p, boolean secureEncoder,
+            byte[] userSecret, String encoderId) {
+        // Use Rappor to get PRR result. Rappor's P and Q are set to 0 and 1 so IRR will not be
+        // effective.
+        // As Rappor has rapporF/2 chance returns 0, rapporF/2 chance returns 1, and 1-rapporF
+        // chance returns original input.
+        // If p < 0.5, setting rapporF=2p and input=0 will make Rappor has p chance to return 1
+        // P(output=1 | input=0) = rapporF/2 = 2p/2 = p.
+        // If p >= 0.5, setting rapporF=2(1-p) and input=1 will make Rappor has p chance
+        // to return 1.
+        // P(output=1 | input=1) = rapporF/2 + (1 - rapporF) = 2(1-p)/2 + (1 - 2(1-p)) = p.
+        final double effectiveF = p < 0.5f ? p * 2 : (1 - p) * 2;
+        final boolean prrInput = p < 0.5f ? false : true;
+        final RapporConfig prrConfig = new RapporConfig(encoderId, 1, effectiveF,
+                0, 1, 1, 1);
+        final RapporEncoder encoder = secureEncoder
+                ? RapporEncoder.createEncoder(prrConfig, userSecret)
+                : RapporEncoder.createInsecureEncoderForTest(prrConfig);
+        return encoder.encodeBoolean(prrInput)[0] > 0;
+    }
+}
diff --git a/core/java/android/privacy/internal/rappor/RapporConfig.java b/core/java/android/privacy/internal/rappor/RapporConfig.java
new file mode 100644
index 0000000..221999b
--- /dev/null
+++ b/core/java/android/privacy/internal/rappor/RapporConfig.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package android.privacy.internal.rappor;
+
+import android.privacy.DifferentialPrivacyConfig;
+import android.text.TextUtils;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * A class to store {@link RapporEncoder} config.
+ *
+ * @hide
+ */
+public class RapporConfig implements DifferentialPrivacyConfig {
+
+    private static final String ALGORITHM_NAME = "Rappor";
+
+    final String mEncoderId;
+    final int mNumBits;
+    final double mProbabilityF;
+    final double mProbabilityP;
+    final double mProbabilityQ;
+    final int mNumCohorts;
+    final int mNumBloomHashes;
+
+    /**
+     * Constructor for {@link RapporConfig}.
+     *
+     * @param encoderId      Unique id for encoder.
+     * @param numBits        Number of bits to be encoded in Rappor algorithm.
+     * @param probabilityF   Probability F that used in Rappor algorithm. This will be
+     *                       quantized to the nearest 1/128.
+     * @param probabilityP   Probability P that used in Rappor algorithm.
+     * @param probabilityQ   Probability Q that used in Rappor algorithm.
+     * @param numCohorts     Number of cohorts that used in Rappor algorithm.
+     * @param numBloomHashes Number of bloom hashes that used in Rappor algorithm.
+     */
+    public RapporConfig(String encoderId, int numBits, double probabilityF,
+            double probabilityP, double probabilityQ, int numCohorts, int numBloomHashes) {
+        Preconditions.checkArgument(!TextUtils.isEmpty(encoderId), "encoderId cannot be empty");
+        this.mEncoderId = encoderId;
+        Preconditions.checkArgument(numBits > 0, "numBits needs to be > 0");
+        this.mNumBits = numBits;
+        Preconditions.checkArgument(probabilityF >= 0 && probabilityF <= 1,
+                "probabilityF must be in range [0.0, 1.0]");
+        this.mProbabilityF = probabilityF;
+        Preconditions.checkArgument(probabilityP >= 0 && probabilityP <= 1,
+                "probabilityP must be in range [0.0, 1.0]");
+        this.mProbabilityP = probabilityP;
+        Preconditions.checkArgument(probabilityQ >= 0 && probabilityQ <= 1,
+                "probabilityQ must be in range [0.0, 1.0]");
+        this.mProbabilityQ = probabilityQ;
+        Preconditions.checkArgument(numCohorts > 0, "numCohorts needs to be > 0");
+        this.mNumCohorts = numCohorts;
+        Preconditions.checkArgument(numBloomHashes > 0, "numBloomHashes needs to be > 0");
+        this.mNumBloomHashes = numBloomHashes;
+    }
+
+    @Override
+    public String getAlgorithm() {
+        return ALGORITHM_NAME;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "EncoderId: %s, NumBits: %d, ProbabilityF: %.3f, ProbabilityP: %.3f"
+                        + ", ProbabilityQ: %.3f, NumCohorts: %d, NumBloomHashes: %d",
+                mEncoderId, mNumBits, mProbabilityF, mProbabilityP, mProbabilityQ,
+                mNumCohorts, mNumBloomHashes);
+    }
+}
diff --git a/core/java/android/privacy/internal/rappor/RapporEncoder.java b/core/java/android/privacy/internal/rappor/RapporEncoder.java
new file mode 100644
index 0000000..2eca4c98
--- /dev/null
+++ b/core/java/android/privacy/internal/rappor/RapporEncoder.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2017 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.
+ */
+
+package android.privacy.internal.rappor;
+
+import android.privacy.DifferentialPrivacyEncoder;
+
+import com.google.android.rappor.Encoder;
+
+import java.security.SecureRandom;
+import java.util.Random;
+
+/**
+ * Differential privacy encoder by using
+ * <a href="https://research.google.com/pubs/pub42852.html">RAPPOR</a>
+ * algorithm.
+ *
+ * @hide
+ */
+public class RapporEncoder implements DifferentialPrivacyEncoder {
+
+    // Hard-coded seed and secret for insecure encoder
+    private static final long INSECURE_RANDOM_SEED = 0x12345678L;
+    private static final byte[] INSECURE_SECRET = new byte[]{
+            (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
+            (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
+            (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+            (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+            (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
+            (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
+            (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+            (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+            (byte) 0xD7, (byte) 0x68, (byte) 0x99, (byte) 0x93,
+            (byte) 0x94, (byte) 0x13, (byte) 0x53, (byte) 0x54,
+            (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54,
+            (byte) 0xFE, (byte) 0xD0, (byte) 0x7E, (byte) 0x54
+    };
+    private static final SecureRandom sSecureRandom = new SecureRandom();
+
+    private final RapporConfig mConfig;
+
+    // Rappor encoder
+    private final Encoder mEncoder;
+    // True if encoder is secure (seed is securely randomized)
+    private final boolean mIsSecure;
+
+
+    private RapporEncoder(RapporConfig config, boolean secureEncoder, byte[] userSecret) {
+        mConfig = config;
+        mIsSecure = secureEncoder;
+        final Random random;
+        if (secureEncoder) {
+            // Use SecureRandom as random generator.
+            random = sSecureRandom;
+        } else {
+            // Hard-coded random generator, to have deterministic result.
+            random = new Random(INSECURE_RANDOM_SEED);
+            userSecret = INSECURE_SECRET;
+        }
+        mEncoder = new Encoder(random, null, null,
+                userSecret, config.mEncoderId, config.mNumBits,
+                config.mProbabilityF, config.mProbabilityP, config.mProbabilityQ,
+                config.mNumCohorts, config.mNumBloomHashes);
+    }
+
+    /**
+     * Create {@link RapporEncoder} with {@link RapporConfig} and user secret provided.
+     *
+     * @param config     Rappor parameters to encode input.
+     * @param userSecret Per device unique secret key.
+     * @return {@link RapporEncoder} instance.
+     */
+    public static RapporEncoder createEncoder(RapporConfig config, byte[] userSecret) {
+        return new RapporEncoder(config, true, userSecret);
+    }
+
+    /**
+     * Create <strong>insecure</strong> {@link RapporEncoder} with {@link RapporConfig} provided.
+     * Should not use it to process sensitive data.
+     *
+     * @param config Rappor parameters to encode input.
+     * @return {@link RapporEncoder} instance.
+     */
+    public static RapporEncoder createInsecureEncoderForTest(RapporConfig config) {
+        return new RapporEncoder(config, false, null);
+    }
+
+    @Override
+    public byte[] encodeString(String original) {
+        return mEncoder.encodeString(original);
+    }
+
+    @Override
+    public byte[] encodeBoolean(boolean original) {
+        return mEncoder.encodeBoolean(original);
+    }
+
+    @Override
+    public byte[] encodeBits(byte[] bits) {
+        return mEncoder.encodeBits(bits);
+    }
+
+    @Override
+    public RapporConfig getConfig() {
+        return mConfig;
+    }
+
+    @Override
+    public boolean isInsecureEncoderForTest() {
+        return !mIsSecure;
+    }
+}
diff --git a/core/tests/privacytests/Android.mk b/core/tests/privacytests/Android.mk
new file mode 100644
index 0000000..7b11225
--- /dev/null
+++ b/core/tests/privacytests/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# Include all test java files.
+LOCAL_SRC_FILES := \
+    $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := junit rappor-tests android-support-test
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_PACKAGE_NAME := FrameworksPrivacyLibraryTests
+
+include $(BUILD_PACKAGE)
diff --git a/core/tests/privacytests/AndroidManifest.xml b/core/tests/privacytests/AndroidManifest.xml
new file mode 100644
index 0000000..a0e5281
--- /dev/null
+++ b/core/tests/privacytests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.frameworks.coretests.privacy">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.frameworks.coretests.privacy"
+            android:label="Frameworks Privacy Library Tests" />
+
+</manifest>
diff --git a/core/tests/privacytests/src/android/privacy/LongitudinalReportingEncoderTest.java b/core/tests/privacytests/src/android/privacy/LongitudinalReportingEncoderTest.java
new file mode 100644
index 0000000..9166438
--- /dev/null
+++ b/core/tests/privacytests/src/android/privacy/LongitudinalReportingEncoderTest.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+package android.privacy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.privacy.internal.longitudinalreporting.LongitudinalReportingConfig;
+import android.privacy.internal.longitudinalreporting.LongitudinalReportingEncoder;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+/**
+ * Unit test for the {@link LongitudinalReportingEncoder}.
+ *
+ * As {@link LongitudinalReportingEncoder} is based on Rappor,
+ * most cases are covered by Rappor tests already.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LongitudinalReportingEncoderTest {
+
+    @Test
+    public void testLongitudinalReportingEncoder_config() throws Exception {
+        final LongitudinalReportingConfig config = new LongitudinalReportingConfig(
+                "Foo",  // encoderId
+                0.4,  // probabilityF
+                0.25,  // probabilityP
+                1);  // probabilityQ
+        final LongitudinalReportingEncoder encoder =
+                LongitudinalReportingEncoder.createInsecureEncoderForTest(
+                        config);
+        assertEquals("LongitudinalReporting", encoder.getConfig().getAlgorithm());
+        assertEquals(
+                "EncoderId: Foo, ProbabilityF: 0.400, ProbabilityP: 0.250, ProbabilityQ: 1.000",
+                encoder.getConfig().toString());
+    }
+
+    @Test
+    public void testLongitudinalReportingEncoder_basicIRRTest() throws Exception {
+        // Test if IRR can generate expected result when seed is fixed (insecure encoder)
+        final LongitudinalReportingConfig config = new LongitudinalReportingConfig(
+                "Foo",  // encoderId
+                0.4,  // probabilityF
+                0,  // probabilityP
+                0);  // probabilityQ
+        // Use insecure encoder here to make sure seed is set.
+        final LongitudinalReportingEncoder encoder =
+                LongitudinalReportingEncoder.createInsecureEncoderForTest(
+                        config);
+        assertEquals(1, encoder.encodeBoolean(true)[0]);
+        assertEquals(0, encoder.encodeBoolean(true)[0]);
+        assertEquals(1, encoder.encodeBoolean(true)[0]);
+        assertEquals(1, encoder.encodeBoolean(true)[0]);
+        assertEquals(1, encoder.encodeBoolean(true)[0]);
+        assertEquals(1, encoder.encodeBoolean(true)[0]);
+        assertEquals(0, encoder.encodeBoolean(true)[0]);
+        assertEquals(1, encoder.encodeBoolean(true)[0]);
+        assertEquals(1, encoder.encodeBoolean(true)[0]);
+        assertEquals(1, encoder.encodeBoolean(true)[0]);
+
+        assertEquals(0, encoder.encodeBoolean(false)[0]);
+        assertEquals(1, encoder.encodeBoolean(false)[0]);
+        assertEquals(1, encoder.encodeBoolean(false)[0]);
+        assertEquals(0, encoder.encodeBoolean(false)[0]);
+        assertEquals(0, encoder.encodeBoolean(false)[0]);
+        assertEquals(0, encoder.encodeBoolean(false)[0]);
+        assertEquals(1, encoder.encodeBoolean(false)[0]);
+        assertEquals(0, encoder.encodeBoolean(false)[0]);
+        assertEquals(0, encoder.encodeBoolean(false)[0]);
+        assertEquals(1, encoder.encodeBoolean(false)[0]);
+
+        // Test if IRR returns original result when f = 0
+        final LongitudinalReportingConfig config2 = new LongitudinalReportingConfig(
+                "Foo",  // encoderId
+                0,  // probabilityF
+                0,  // probabilityP
+                0);  // probabilityQ
+        final LongitudinalReportingEncoder encoder2
+                = LongitudinalReportingEncoder.createEncoder(
+                config2, makeTestingUserSecret("secret2"));
+        for (int i = 0; i < 10; i++) {
+            assertEquals(1, encoder2.encodeBoolean(true)[0]);
+        }
+        for (int i = 0; i < 10; i++) {
+            assertEquals(0, encoder2.encodeBoolean(false)[0]);
+        }
+
+        // Test if IRR returns opposite result when f = 1
+        final LongitudinalReportingConfig config3 = new LongitudinalReportingConfig(
+                "Foo",  // encoderId
+                1,  // probabilityF
+                0,  // probabilityP
+                0);  // probabilityQ
+        final LongitudinalReportingEncoder encoder3
+                = LongitudinalReportingEncoder.createEncoder(
+                config3, makeTestingUserSecret("secret3"));
+        for (int i = 0; i < 10; i++) {
+            assertEquals(1, encoder3.encodeBoolean(false)[0]);
+        }
+        for (int i = 0; i < 10; i++) {
+            assertEquals(0, encoder3.encodeBoolean(true)[0]);
+        }
+    }
+
+    @Test
+    public void testLongitudinalReportingEncoder_basicPRRTest() throws Exception {
+        // Should always return original value when p = 0
+        for (int i = 0; i < 10; i++) {
+            for (int j = 0; j < 10; j++) {
+                final LongitudinalReportingConfig config1 = new LongitudinalReportingConfig(
+                        "Foo" + i,  // encoderId
+                        0,  // probabilityF
+                        0,  // probabilityP
+                        0);  // probabilityQ
+                final LongitudinalReportingEncoder encoder1
+                        = LongitudinalReportingEncoder.createEncoder(
+                        config1, makeTestingUserSecret("encoder" + j));
+                assertEquals(0, encoder1.encodeBoolean(false)[0]);
+                assertEquals(0, encoder1.encodeBoolean(false)[0]);
+                assertEquals(0, encoder1.encodeBoolean(false)[0]);
+                assertEquals(0, encoder1.encodeBoolean(false)[0]);
+                assertEquals(0, encoder1.encodeBoolean(false)[0]);
+                assertEquals(1, encoder1.encodeBoolean(true)[0]);
+                assertEquals(1, encoder1.encodeBoolean(true)[0]);
+                assertEquals(1, encoder1.encodeBoolean(true)[0]);
+                assertEquals(1, encoder1.encodeBoolean(true)[0]);
+                assertEquals(1, encoder1.encodeBoolean(true)[0]);
+            }
+        }
+
+        // Should always return false when p = 1, q = 0
+        for (int i = 0; i < 10; i++) {
+            for (int j = 0; j < 10; j++) {
+                final LongitudinalReportingConfig config2 = new LongitudinalReportingConfig(
+                        "Foo" + i,  // encoderId
+                        0,  // probabilityF
+                        1,  // probabilityP
+                        0);  // probabilityQ
+                final LongitudinalReportingEncoder encoder2
+                        = LongitudinalReportingEncoder.createEncoder(
+                        config2, makeTestingUserSecret("encoder" + j));
+                assertEquals(0, encoder2.encodeBoolean(false)[0]);
+                assertEquals(0, encoder2.encodeBoolean(false)[0]);
+                assertEquals(0, encoder2.encodeBoolean(false)[0]);
+                assertEquals(0, encoder2.encodeBoolean(false)[0]);
+                assertEquals(0, encoder2.encodeBoolean(false)[0]);
+                assertEquals(0, encoder2.encodeBoolean(true)[0]);
+                assertEquals(0, encoder2.encodeBoolean(true)[0]);
+                assertEquals(0, encoder2.encodeBoolean(true)[0]);
+                assertEquals(0, encoder2.encodeBoolean(true)[0]);
+                assertEquals(0, encoder2.encodeBoolean(true)[0]);
+            }
+        }
+
+        // Should always return true when p = 1, q = 1
+        for (int i = 0; i < 10; i++) {
+            for (int j = 0; j < 10; j++) {
+                final LongitudinalReportingConfig config3 = new LongitudinalReportingConfig(
+                        "Foo" + i,  // encoderId
+                        0,  // probabilityF
+                        1,  // probabilityP
+                        1);  // probabilityQ
+                final LongitudinalReportingEncoder encoder3
+                        = LongitudinalReportingEncoder.createEncoder(
+                        config3, makeTestingUserSecret("encoder" + j));
+                assertEquals(1, encoder3.encodeBoolean(false)[0]);
+                assertEquals(1, encoder3.encodeBoolean(false)[0]);
+                assertEquals(1, encoder3.encodeBoolean(false)[0]);
+                assertEquals(1, encoder3.encodeBoolean(false)[0]);
+                assertEquals(1, encoder3.encodeBoolean(false)[0]);
+                assertEquals(1, encoder3.encodeBoolean(true)[0]);
+                assertEquals(1, encoder3.encodeBoolean(true)[0]);
+                assertEquals(1, encoder3.encodeBoolean(true)[0]);
+                assertEquals(1, encoder3.encodeBoolean(true)[0]);
+                assertEquals(1, encoder3.encodeBoolean(true)[0]);
+            }
+        }
+
+        // PRR should return different value when encoder id is changed
+        boolean hasFalseResult1 = false;
+        boolean hasTrueResult1 = false;
+        for (int i = 0; i < 50; i++) {
+            boolean firstResult = false;
+            for (int j = 0; j < 10; j++) {
+                final LongitudinalReportingConfig config4 = new LongitudinalReportingConfig(
+                        "Foo" + i,  // encoderId
+                        0,  // probabilityF
+                        1,  // probabilityP
+                        0.5);  // probabilityQ
+                final LongitudinalReportingEncoder encoder4
+                        = LongitudinalReportingEncoder.createEncoder(
+                        config4, makeTestingUserSecret("encoder4"));
+                boolean encodedFalse = encoder4.encodeBoolean(false)[0] > 0;
+                boolean encodedTrue = encoder4.encodeBoolean(true)[0] > 0;
+                // PRR should always give the same value when all parameters are the same
+                assertEquals(encodedTrue, encodedFalse);
+                if (j == 0) {
+                    firstResult = encodedTrue;
+                } else {
+                    assertEquals(firstResult, encodedTrue);
+                }
+                if (encodedTrue) {
+                    hasTrueResult1 = true;
+                } else {
+                    hasFalseResult1 = true;
+                }
+            }
+        }
+        // Ensure it has both true and false results when encoder id is different
+        assertTrue(hasTrueResult1);
+        assertTrue(hasFalseResult1);
+
+        // PRR should give different value when secret is changed
+        boolean hasFalseResult2 = false;
+        boolean hasTrueResult2 = false;
+        for (int i = 0; i < 50; i++) {
+            boolean firstResult = false;
+            for (int j = 0; j < 10; j++) {
+                final LongitudinalReportingConfig config5 = new LongitudinalReportingConfig(
+                        "Foo",  // encoderId
+                        0,  // probabilityF
+                        1,  // probabilityP
+                        0.5);  // probabilityQ
+                final LongitudinalReportingEncoder encoder5
+                        = LongitudinalReportingEncoder.createEncoder(
+                        config5, makeTestingUserSecret("encoder" + i));
+                boolean encodedFalse = encoder5.encodeBoolean(false)[0] > 0;
+                boolean encodedTrue = encoder5.encodeBoolean(true)[0] > 0;
+                // PRR should always give the same value when parameters are the same
+                assertEquals(encodedTrue, encodedFalse);
+                if (j == 0) {
+                    firstResult = encodedTrue;
+                } else {
+                    assertEquals(firstResult, encodedTrue);
+                }
+                if (encodedTrue) {
+                    hasTrueResult2 = true;
+                } else {
+                    hasFalseResult2 = true;
+                }
+            }
+        }
+        // Ensure it has both true and false results when encoder id is different
+        assertTrue(hasTrueResult2);
+        assertTrue(hasFalseResult2);
+
+        // Confirm if PRR randomizer is working correctly
+        final int n1 = 1000;
+        final double p1 = 0.8;
+        final double expectedTrueSum1 = n1 * p1;
+        final double valueRange1 = 5 * Math.sqrt(n1 * p1 * (1 - p1));
+        int trueSum1 = 0;
+        for (int i = 0; i < n1; i++) {
+            final LongitudinalReportingConfig config6 = new LongitudinalReportingConfig(
+                    "Foo",  // encoderId
+                    0,  // probabilityF
+                    p1,  // probabilityP
+                    1);  // probabilityQ
+            final LongitudinalReportingEncoder encoder6
+                    = LongitudinalReportingEncoder.createEncoder(
+                    config6, makeTestingUserSecret("encoder" + i));
+            boolean encodedFalse = encoder6.encodeBoolean(false)[0] > 0;
+            if (encodedFalse) {
+                trueSum1 += 1;
+            }
+        }
+        // Total number of true(s) should be around the mean (1000 * 0.8)
+        assertTrue(trueSum1 < expectedTrueSum1 + valueRange1);
+        assertTrue(trueSum1 > expectedTrueSum1 - valueRange1);
+
+        // Confirm if PRR randomizer is working correctly
+        final int n2 = 1000;
+        final double p2 = 0.2;
+        final double expectedTrueSum2 = n2 * p2;
+        final double valueRange2 = 5 * Math.sqrt(n2 * p2 * (1 - p2));
+        int trueSum2 = 0;
+        for (int i = 0; i < n2; i++) {
+            final LongitudinalReportingConfig config7 = new LongitudinalReportingConfig(
+                    "Foo",  // encoderId
+                    0,  // probabilityF
+                    p2,  // probabilityP
+                    1);  // probabilityQ
+            final LongitudinalReportingEncoder encoder7
+                    = LongitudinalReportingEncoder.createEncoder(
+                    config7, makeTestingUserSecret("encoder" + i));
+            boolean encodedFalse = encoder7.encodeBoolean(false)[0] > 0;
+            if (encodedFalse) {
+                trueSum2 += 1;
+            }
+        }
+        // Total number of true(s) should be around the mean (1000 * 0.2)
+        assertTrue(trueSum2 < expectedTrueSum2 + valueRange2);
+        assertTrue(trueSum2 > expectedTrueSum2 - valueRange2);
+    }
+
+    @Test
+    public void testLongitudinalReportingEncoder_basicIRRwithPRRTest() throws Exception {
+        // Verify PRR result will run IRR
+        boolean hasFalseResult1 = false;
+        boolean hasTrueResult1 = false;
+        for (int i = 0; i < 50; i++) {
+            final LongitudinalReportingConfig config1 = new LongitudinalReportingConfig(
+                    "Foo",  // encoderId
+                    0.5,  // probabilityF
+                    1,  // probabilityP
+                    1);  // probabilityQ
+            final LongitudinalReportingEncoder encoder1
+                    = LongitudinalReportingEncoder.createEncoder(
+                    config1, makeTestingUserSecret("encoder1"));
+            if (encoder1.encodeBoolean(false)[0] > 0) {
+                hasTrueResult1 = true;
+            } else {
+                hasFalseResult1 = true;
+            }
+        }
+        assertTrue(hasTrueResult1);
+        assertTrue(hasFalseResult1);
+
+        // When secret is different, some device should use PRR result, some should use IRR result
+        boolean hasFalseResult2 = false;
+        boolean hasTrueResult2 = false;
+        for (int i = 0; i < 50; i++) {
+            final LongitudinalReportingConfig config2 = new LongitudinalReportingConfig(
+                    "Foo",  // encoderId
+                    1,  // probabilityF
+                    0.5,  // probabilityP
+                    1);  // probabilityQ
+            final LongitudinalReportingEncoder encoder2
+                    = LongitudinalReportingEncoder.createEncoder(
+                    config2, makeTestingUserSecret("encoder" + i));
+            if (encoder2.encodeBoolean(false)[0] > 0) {
+                hasTrueResult2 = true;
+            } else {
+                hasFalseResult2 = true;
+            }
+        }
+        assertTrue(hasTrueResult2);
+        assertTrue(hasFalseResult2);
+    }
+
+    @Test
+    public void testLongTermRandomizedResult() throws Exception {
+        // Verify getLongTermRandomizedResult can return expected result when parameters are fixed.
+        final boolean[] expectedResult =
+                new boolean[]{true, false, true, true, true,
+                        false, false, false, true, false,
+                        false, false, false, true, true,
+                        true, true, false, true, true,
+                        true, true, false, true, true};
+        for (int i = 0; i < 5; i++) {
+            for (int j = 0; j < 5; j++) {
+                boolean result = LongitudinalReportingEncoder.getLongTermRandomizedResult(0.5,
+                        true, makeTestingUserSecret("secret" + i), "encoder" + j);
+                assertEquals(expectedResult[i * 5 + j], result);
+            }
+        }
+    }
+
+    private static byte[] makeTestingUserSecret(String testingSecret) throws Exception {
+        // We generate the fake user secret by concatenating three copies of the
+        // 16 byte MD5 hash of the testingSecret string encoded in UTF 8.
+        MessageDigest md5 = MessageDigest.getInstance("MD5");
+        byte[] digest = md5.digest(testingSecret.getBytes(StandardCharsets.UTF_8));
+        assertEquals(16, digest.length);
+        return ByteBuffer.allocate(48).put(digest).put(digest).put(digest).array();
+    }
+}
diff --git a/core/tests/privacytests/src/android/privacy/RapporEncoderTest.java b/core/tests/privacytests/src/android/privacy/RapporEncoderTest.java
new file mode 100644
index 0000000..dad98b8
--- /dev/null
+++ b/core/tests/privacytests/src/android/privacy/RapporEncoderTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+package android.privacy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.privacy.internal.rappor.RapporConfig;
+import android.privacy.internal.rappor.RapporEncoder;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+/**
+ * Unit test for the {@link RapporEncoder}.
+ * Most of the tests are done in external/rappor/client/javatest/ already.
+ * Tests here are just make sure the {@link RapporEncoder} wrap Rappor correctly.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class RapporEncoderTest {
+
+    @Test
+    public void testRapporEncoder_config() throws Exception {
+        final RapporConfig config = new RapporConfig(
+                "Foo",  // encoderId
+                8,  // numBits,
+                13.0 / 128.0,  // probabilityF
+                0.25,  // probabilityP
+                0.75,  // probabilityQ
+                1,  // numCohorts
+                2);  // numBloomHashes)
+        final RapporEncoder encoder = RapporEncoder.createEncoder(config,
+                makeTestingUserSecret("encoder1"));
+        assertEquals("Rappor", encoder.getConfig().getAlgorithm());
+        assertEquals("EncoderId: Foo, NumBits: 8, ProbabilityF: 0.102, "
+                + "ProbabilityP: 0.250, ProbabilityQ: 0.750, NumCohorts: 1, "
+                + "NumBloomHashes: 2", encoder.getConfig().toString());
+    }
+
+    @Test
+    public void testRapporEncoder_basicIRRTest() throws Exception {
+        final RapporConfig config = new RapporConfig(
+                "Foo", // encoderId
+                12, // numBits,
+                0, // probabilityF
+                0, // probabilityP
+                1, // probabilityQ
+                1, // numCohorts (so must be cohort 0)
+                2);  // numBloomHashes
+        // Use insecure encoder here as we want to get the exact output.
+        final RapporEncoder encoder = RapporEncoder.createInsecureEncoderForTest(config);
+        assertEquals(768, toLong(encoder.encodeString("Testing")));
+    }
+
+    @Test
+    public void testRapporEncoder_IRRWithPRR() throws Exception {
+        int numBits = 8;
+        final long inputValue = 254L;
+        final long prrValue = 250L;
+        final long prrAndIrrValue = 184L;
+
+        final RapporConfig config1 = new RapporConfig(
+                "Foo", // encoderId
+                numBits, // numBits,
+                0.25, // probabilityF
+                0, // probabilityP
+                1, // probabilityQ
+                1, // numCohorts
+                2); // numBloomHashes
+        // Use insecure encoder here as we want to get the exact output.
+        final RapporEncoder encoder1 = RapporEncoder.createInsecureEncoderForTest(config1);
+        // Verify that PRR is working as expected.
+        assertEquals(prrValue, toLong(encoder1.encodeBits(toBytes(inputValue))));
+        assertTrue(encoder1.isInsecureEncoderForTest());
+
+        // Verify that IRR is working as expected.
+        final RapporConfig config2 = new RapporConfig(
+                "Foo", // encoderId
+                numBits, // numBits,
+                0, // probabilityF
+                0.3, // probabilityP
+                0.7, // probabilityQ
+                1, // numCohorts
+                2); // numBloomHashes
+        // Use insecure encoder here as we want to get the exact output.
+        final RapporEncoder encoder2 = RapporEncoder.createInsecureEncoderForTest(config2);
+        assertEquals(prrAndIrrValue, toLong(encoder2.encodeBits(toBytes(prrValue))));
+
+        // Test that end-to-end is the result of PRR + IRR.
+        final RapporConfig config3 = new RapporConfig(
+                "Foo", // encoderId
+                numBits, // numBits,
+                0.25, // probabilityF
+                0.3, // probabilityP
+                0.7, // probabilityQ
+                1, // numCohorts
+                2); // numBloomHashes
+        final RapporEncoder encoder3 = RapporEncoder.createInsecureEncoderForTest(config3);
+        // Verify that PRR is working as expected.
+        assertEquals(prrAndIrrValue, toLong(encoder3.encodeBits(toBytes(inputValue))));
+    }
+
+    @Test
+    public void testRapporEncoder_ensureSecureEncoderIsSecure() throws Exception {
+        int numBits = 8;
+        final long inputValue = 254L;
+        final long prrValue = 250L;
+        final long prrAndIrrValue = 184L;
+
+        final RapporConfig config1 = new RapporConfig(
+                "Foo", // encoderId
+                numBits, // numBits,
+                0.25, // probabilityF
+                0, // probabilityP
+                1, // probabilityQ
+                1, // numCohorts
+                2); // numBloomHashes
+        final RapporEncoder encoder1 = RapporEncoder.createEncoder(config1,
+                makeTestingUserSecret("secret1"));
+        // Verify that PRR is working as expected, not affected by random seed.
+        assertEquals(prrValue, toLong(encoder1.encodeBits(toBytes(inputValue))));
+        assertFalse(encoder1.isInsecureEncoderForTest());
+
+        boolean hasDifferentResult2 = false;
+        for (int i = 0; i < 5; i++) {
+            final RapporConfig config2 = new RapporConfig(
+                    "Foo", // encoderId
+                    numBits, // numBits,
+                    0, // probabilityF
+                    0.3, // probabilityP
+                    0.7, // probabilityQ
+                    1, // numCohorts
+                    2); // numBloomHashes
+            final RapporEncoder encoder2 = RapporEncoder.createEncoder(config2,
+                    makeTestingUserSecret("secret1"));
+            hasDifferentResult2 |= (prrAndIrrValue != toLong(
+                    encoder2.encodeBits(toBytes(prrValue))));
+        }
+        // Ensure it's not getting same result as it has random seed while encoder id and secret
+        // is the same.
+        assertTrue(hasDifferentResult2);
+
+        boolean hasDifferentResults3 = false;
+        for (int i = 0; i < 5; i++) {
+            final RapporConfig config3 = new RapporConfig(
+                    "Foo", // encoderId
+                    numBits, // numBits,
+                    0.25, // probabilityF
+                    0.3, // probabilityP
+                    0.7, // probabilityQ
+                    1, // numCohorts
+                    2); // numBloomHashes
+            final RapporEncoder encoder3 = RapporEncoder.createEncoder(config3,
+                    makeTestingUserSecret("secret1"));
+            hasDifferentResults3 |= (prrAndIrrValue != toLong(
+                    encoder3.encodeBits(toBytes(inputValue))));
+        }
+        // Ensure it's not getting same result as it has random seed while encoder id and secret
+        // is the same.
+        assertTrue(hasDifferentResults3);
+    }
+
+    private static byte[] toBytes(long value) {
+        return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).array();
+    }
+
+    private static long toLong(byte[] bytes) {
+        ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).put(bytes);
+        buffer.rewind();
+        return buffer.getLong();
+    }
+
+    private static byte[] makeTestingUserSecret(String testingSecret) throws Exception {
+        // We generate the fake user secret by concatenating three copies of the
+        // 16 byte MD5 hash of the testingSecret string encoded in UTF 8.
+        MessageDigest md5 = MessageDigest.getInstance("MD5");
+        byte[] digest = md5.digest(testingSecret.getBytes(StandardCharsets.UTF_8));
+        assertEquals(16, digest.length);
+        return ByteBuffer.allocate(48).put(digest).put(digest).put(digest).array();
+    }
+}