Merge "SystemAPI for ACTION_SMS_EMERGENCY_CB_RECEIVED intent"
diff --git a/Android.bp b/Android.bp
index cbd991a..5675fa2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11,15 +11,24 @@
     certificate: "platform",
     privileged: true,
     resource_dirs: ["res"],
-    static_libs: [
-        "androidx.legacy_legacy-support-v4",
-        "androidx.legacy_legacy-support-v13",
-        "androidx.recyclerview_recyclerview",
-        "androidx.preference_preference",
-        "androidx.appcompat_appcompat",
-        "androidx.legacy_legacy-preference-v14",
-    ],
     optimize: {
         proguard_flags_files: ["proguard.flags"],
     },
 }
+
+// used to share common constants between cellbroadcastservice and cellbroadcastreceier
+filegroup {
+    name: "cellbroadcast-constants-shared-srcs",
+    srcs: [
+        "src/com/android/cellbroadcastservice/SmsCbConstants.java",
+    ],
+}
+
+// used to share src with unit test app
+filegroup {
+    name: "cellbroadcast-shared-srcs",
+    srcs: [
+        "src/**/*.java",
+        ":framework-cellbroadcast-shared-srcs",
+    ],
+}
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2dd2140..4dc0b97 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -38,12 +38,16 @@
             android:process="com.android.phone">
 
         <service android:name="DefaultCellBroadcastService"
-                android:process="com.android.phone"
                 android:exported="true"
                 android:permission="android.permission.BIND_CELL_BROADCAST_SERVICE">
             <intent-filter>
                 <action android:name="android.telephony.CellBroadcastService" />
             </intent-filter>
         </service>
+        <provider android:name="CellBroadcastProvider"
+                android:authorities="cellbroadcasts"
+                android:exported="true"
+                android:singleUser="true"
+                android:multiprocess="false" />
     </application>
 </manifest>
diff --git a/res/values-mcc310-mnc170 b/res/values-mcc310-mnc170
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc310-mnc170
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc310-mnc380 b/res/values-mcc310-mnc380
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc310-mnc380
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc310-mnc410/config.xml b/res/values-mcc310-mnc410/config.xml
new file mode 100644
index 0000000..2301d02
--- /dev/null
+++ b/res/values-mcc310-mnc410/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<resources>
+    <!-- Whether to reset alert message duplicate detection after toggling airplane mode -->
+    <bool name="reset_duplicate_detection_on_airplane_mode">true</bool>
+</resources>
diff --git a/res/values-mcc310-mnc560 b/res/values-mcc310-mnc560
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc310-mnc560
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc310-mnc680 b/res/values-mcc310-mnc680
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc310-mnc680
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc310-mnc70 b/res/values-mcc310-mnc70
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc310-mnc70
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc311-mnc180 b/res/values-mcc311-mnc180
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc311-mnc180
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc312-mnc670 b/res/values-mcc312-mnc670
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc312-mnc670
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc313-mnc100 b/res/values-mcc313-mnc100
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc313-mnc100
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc313-mnc110 b/res/values-mcc313-mnc110
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc313-mnc110
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc313-mnc120 b/res/values-mcc313-mnc120
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc313-mnc120
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc313-mnc130 b/res/values-mcc313-mnc130
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc313-mnc130
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc313-mnc140 b/res/values-mcc313-mnc140
new file mode 120000
index 0000000..78e6503
--- /dev/null
+++ b/res/values-mcc313-mnc140
@@ -0,0 +1 @@
+values-mcc310-mnc410
\ No newline at end of file
diff --git a/res/values-mcc440-mnc11/config.xml b/res/values-mcc440-mnc11/config.xml
new file mode 100644
index 0000000..9866fee
--- /dev/null
+++ b/res/values-mcc440-mnc11/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<resources>
+    <!-- The message expiration time in milliseconds for duplicate detection -->
+    <integer name="message_expiration_time">600000</integer>
+</resources>
diff --git a/res/values-mcc440-mnc20/config.xml b/res/values-mcc440-mnc20/config.xml
new file mode 100644
index 0000000..9866fee
--- /dev/null
+++ b/res/values-mcc440-mnc20/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<resources>
+    <!-- The message expiration time in milliseconds for duplicate detection -->
+    <integer name="message_expiration_time">600000</integer>
+</resources>
diff --git a/res/values-mcc440-mnc50/config.xml b/res/values-mcc440-mnc50/config.xml
new file mode 100644
index 0000000..9866fee
--- /dev/null
+++ b/res/values-mcc440-mnc50/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<resources>
+    <!-- The message expiration time in milliseconds for duplicate detection -->
+    <integer name="message_expiration_time">600000</integer>
+</resources>
diff --git a/res/values-mcc440-mnc51 b/res/values-mcc440-mnc51
new file mode 120000
index 0000000..77908a2
--- /dev/null
+++ b/res/values-mcc440-mnc51
@@ -0,0 +1 @@
+values-mcc440-mnc50
\ No newline at end of file
diff --git a/res/values-mcc440-mnc52 b/res/values-mcc440-mnc52
new file mode 120000
index 0000000..77908a2
--- /dev/null
+++ b/res/values-mcc440-mnc52
@@ -0,0 +1 @@
+values-mcc440-mnc50
\ No newline at end of file
diff --git a/res/values-mcc440/config.xml b/res/values-mcc440/config.xml
new file mode 100644
index 0000000..7a2c4bd
--- /dev/null
+++ b/res/values-mcc440/config.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<resources>
+    <!-- The message expiration time in milliseconds for duplicate detection -->
+    <integer name="message_expiration_time">3600000</integer>
+
+    <!-- Whether to compare message body when performing message duplicate detection -->
+    <bool name="duplicate_compare_body">true</bool>
+</resources>
diff --git a/res/values-mcc441 b/res/values-mcc441
new file mode 120000
index 0000000..dab2810
--- /dev/null
+++ b/res/values-mcc441
@@ -0,0 +1 @@
+values-mcc440
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 3ac256a..f8e7112 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -23,4 +23,13 @@
     <string-array name="config_defaultCellBroadcastReceiverPkgs" translatable="false">
         <item>com.android.cellbroadcastreceiver</item>
     </string-array>
+
+    <!-- The message expiration time in milliseconds for duplicate detection -->
+    <integer name="message_expiration_time">86400000</integer>
+
+    <!-- Whether to compare message body when deduping messages -->
+    <bool name="duplicate_compare_body">false</bool>
+
+    <!-- Whether to reset alert message duplicate detection after toggling airplane mode -->
+    <bool name="reset_duplicate_detection_on_airplane_mode">false</bool>
 </resources>
diff --git a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
index 76e690b..5f8a37b 100644
--- a/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
+++ b/src/com/android/cellbroadcastservice/CellBroadcastHandler.java
@@ -16,7 +16,6 @@
 
 package com.android.cellbroadcastservice;
 
-import static android.content.PermissionChecker.PERMISSION_GRANTED;
 import static android.provider.Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG;
 
 import android.Manifest;
@@ -24,10 +23,14 @@
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.AppOpsManager;
+import android.content.BroadcastReceiver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.content.PermissionChecker;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
 import android.location.Location;
 import android.location.LocationListener;
 import android.location.LocationManager;
@@ -37,25 +40,36 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Process;
+import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.provider.Telephony;
 import android.provider.Telephony.CellBroadcasts;
 import android.telephony.SmsCbMessage;
 import android.telephony.SubscriptionManager;
+import android.telephony.cdma.CdmaSmsCbProgramData;
+import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.LocalLog;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telephony.CbGeoUtils.Geometry;
 import com.android.internal.telephony.CbGeoUtils.LatLng;
 import com.android.internal.telephony.metrics.TelephonyMetrics;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Dispatch new Cell Broadcasts to receivers. Acquires a private wakelock until the broadcast
@@ -64,13 +78,45 @@
 public class CellBroadcastHandler extends WakeLockStateMachine {
     private static final String EXTRA_MESSAGE = "message";
 
+    /**
+     * To disable cell broadcast duplicate detection for debugging purposes
+     * <code>adb shell am broadcast -a com.android.cellbroadcastservice.action.DUPLICATE_DETECTION
+     * --ez enable false</code>
+     *
+     * To enable cell broadcast duplicate detection for debugging purposes
+     * <code>adb shell am broadcast -a com.android.cellbroadcastservice.action.DUPLICATE_DETECTION
+     * --ez enable true</code>
+     */
+    private static final String ACTION_DUPLICATE_DETECTION =
+            "com.android.cellbroadcastservice.action.DUPLICATE_DETECTION";
+
+    /**
+     * The extra for cell broadcast duplicate detection enable/disable
+     */
+    private static final String EXTRA_ENABLE = "enable";
+
     private final LocalLog mLocalLog = new LocalLog(100);
 
-    protected static final Uri CELL_BROADCAST_URI = Uri.parse("content://cellbroadcasts_fwk");
+    private static final boolean IS_DEBUGGABLE = SystemProperties.getInt("ro.debuggable", 0) == 1;
 
     /** Uses to request the location update. */
     public final LocationRequester mLocationRequester;
 
+    /** Timestamp of last airplane mode on */
+    private long mLastAirplaneModeTime = 0;
+
+    /** Resource cache */
+    private final Map<Integer, Resources> mResourcesCache = new HashMap<>();
+
+    /** Whether performing duplicate detection or not. Note this is for debugging purposes only. */
+    private boolean mEnableDuplicateDetection = true;
+
+    /**
+     * Service category equivalent map. The key is the GSM service category, the value is the CDMA
+     * service category.
+     */
+    private final Map<Integer, Integer> mServiceCategoryCrossRATMap;
+
     private CellBroadcastHandler(Context context) {
         this("CellBroadcastHandler", context);
     }
@@ -81,6 +127,89 @@
                 context,
                 (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE),
                 getHandler().getLooper());
+
+        // Adding GSM / CDMA service category mapping.
+        mServiceCategoryCrossRATMap = Stream.of(new Integer[][] {
+                // Presidential alert
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT},
+
+                // Extreme alert
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_EXTREME_THREAT},
+
+                // Severe alert
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_SEVERE_THREAT},
+
+                // Amber alert
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY},
+
+                // Monthly test alert
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE},
+                { SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE,
+                        CdmaSmsCbProgramData.CATEGORY_CMAS_TEST_MESSAGE},
+        }).collect(Collectors.toMap(data -> data[0], data -> data[1]));
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        if (Build.IS_DEBUGGABLE) {
+            intentFilter.addAction(ACTION_DUPLICATE_DETECTION);
+        }
+        mContext.registerReceiver(
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        switch (intent.getAction()) {
+                            case Intent.ACTION_AIRPLANE_MODE_CHANGED:
+                                boolean airplaneModeOn = intent.getBooleanExtra("state", false);
+                                if (airplaneModeOn) {
+                                    mLastAirplaneModeTime = System.currentTimeMillis();
+                                    log("Airplane mode on. Reset duplicate detection.");
+                                }
+                                break;
+                            case ACTION_DUPLICATE_DETECTION:
+                                mEnableDuplicateDetection = intent.getBooleanExtra(EXTRA_ENABLE,
+                                        true);
+                                break;
+                        }
+
+                    }
+                }, intentFilter);
     }
 
     /**
@@ -104,8 +233,11 @@
     @Override
     protected boolean handleSmsMessage(Message message) {
         if (message.obj instanceof SmsCbMessage) {
-            handleBroadcastSms((SmsCbMessage) message.obj);
-            return true;
+            if (!isDuplicate((SmsCbMessage) message.obj)) {
+                handleBroadcastSms((SmsCbMessage) message.obj);
+                return true;
+            }
+            return false;
         } else {
             loge("handleMessage got object of type: " + message.obj.getClass().getName());
             return false;
@@ -128,7 +260,7 @@
         // TODO: Database inserting can be time consuming, therefore this should be changed to
         // asynchronous.
         ContentValues cv = message.getContentValues();
-        Uri uri = mContext.getContentResolver().insert(CELL_BROADCAST_URI, cv);
+        Uri uri = mContext.getContentResolver().insert(CellBroadcasts.CONTENT_URI, cv);
 
         if (message.needGeoFencingCheck()) {
             if (DBG) {
@@ -155,6 +287,116 @@
     }
 
     /**
+     * Check if the message is a duplicate
+     *
+     * @param message Cell broadcast message
+     * @return {@code true} if this message is a duplicate
+     */
+    @VisibleForTesting
+    public boolean isDuplicate(SmsCbMessage message) {
+        if (!mEnableDuplicateDetection) {
+            log("Duplicate detection was disabled for debugging purposes.");
+            return false;
+        }
+
+        // Find the cell broadcast message identify by the message identifier and serial number
+        // and is not broadcasted.
+        String where = CellBroadcasts.RECEIVED_TIME + ">?";
+
+        int slotIndex = message.getSlotIndex();
+        SubscriptionManager subMgr = (SubscriptionManager) mContext.getSystemService(
+                Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        int[] subIds = subMgr.getSubscriptionIds(slotIndex);
+        Resources res;
+        if (subIds != null) {
+            res = getResources(subIds[0]);
+        } else {
+            res = getResources(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID);
+        }
+
+        // Only consider cell broadcast messages received within certain period.
+        // By default it's 24 hours.
+        long expirationDuration = res.getInteger(R.integer.message_expiration_time);
+        long dupCheckTime = System.currentTimeMillis() - expirationDuration;
+
+        // Some carriers require reset duplication detection after airplane mode.
+        if (res.getBoolean(R.bool.reset_duplicate_detection_on_airplane_mode)) {
+            dupCheckTime = Long.max(dupCheckTime, mLastAirplaneModeTime);
+        }
+
+        List<SmsCbMessage> cbMessages = new ArrayList<>();
+
+        try (Cursor cursor = mContext.getContentResolver().query(CellBroadcasts.CONTENT_URI,
+                // TODO: QUERY_COLUMNS_FWK is a hidden API, since we are going to move
+                //  CellBroadcastProvider to this module we can define those COLUMNS in side
+                //  CellBroadcastProvider and reference from there.
+                CellBroadcasts.QUERY_COLUMNS_FWK,
+                where,
+                new String[] {Long.toString(dupCheckTime)},
+                null)) {
+            if (cursor != null) {
+                while (cursor.moveToNext()) {
+                    cbMessages.add(SmsCbMessage.createFromCursor(cursor));
+                }
+            }
+        }
+
+        boolean compareMessageBody = res.getBoolean(R.bool.duplicate_compare_body);
+
+        log("Found " + cbMessages.size() + " messages since "
+                + DateFormat.getDateTimeInstance().format(dupCheckTime));
+        for (SmsCbMessage messageToCheck : cbMessages) {
+            // If messages are from different slots, then we only compare the message body.
+            if (message.getSlotIndex() != messageToCheck.getSlotIndex()) {
+                if (TextUtils.equals(message.getMessageBody(), messageToCheck.getMessageBody())) {
+                    log("Duplicate message detected from different slot. " + message);
+                    return true;
+                }
+            } else {
+                // Check serial number if message is from the same carrier.
+                if (message.getSerialNumber() != messageToCheck.getSerialNumber()) {
+                    // Not a dup. Check next one.
+                    log("Serial number check. Not a dup. " + messageToCheck);
+                    continue;
+                }
+
+                // ETWS primary / secondary should be treated differently.
+                if (message.isEtwsMessage() && messageToCheck.isEtwsMessage()
+                        && message.getEtwsWarningInfo().isPrimary()
+                        != messageToCheck.getEtwsWarningInfo().isPrimary()) {
+                    // Not a dup. Check next one.
+                    log("ETWS primary check. Not a dup. " + messageToCheck);
+                    continue;
+                }
+
+                // Check if the message category is different. Some carriers send cell broadcast
+                // messages on different techs (i.e. GSM / CDMA), so we need to compare service
+                // category cross techs.
+                if (message.getServiceCategory() != messageToCheck.getServiceCategory()
+                        && !Objects.equals(mServiceCategoryCrossRATMap.get(
+                                message.getServiceCategory()), messageToCheck.getServiceCategory())
+                        && !Objects.equals(mServiceCategoryCrossRATMap.get(
+                                messageToCheck.getServiceCategory()),
+                        message.getServiceCategory())) {
+                    // Not a dup. Check next one.
+                    log("Service category check. Not a dup. " + messageToCheck);
+                    continue;
+                }
+
+                // Compare message body if needed.
+                if (!compareMessageBody || TextUtils.equals(
+                        message.getMessageBody(), messageToCheck.getMessageBody())) {
+                    log("Duplicate message detected. " + message);
+                    return true;
+                }
+            }
+        }
+
+        log("Not a duplicate message. " + message);
+        return false;
+    }
+
+    /**
      * Perform a geo-fencing check for {@code message}. Broadcast the {@code message} if the
      * {@code location} is inside the {@code broadcastArea}.
      * @param message the message need to geo-fencing check
@@ -182,6 +424,8 @@
             logd("Device location is outside the broadcast area "
                     + CbGeoUtils.encodeGeometriesToString(broadcastArea));
         }
+
+        sendMessage(EVENT_BROADCAST_NOT_REQUIRED);
     }
 
     /**
@@ -195,18 +439,6 @@
     }
 
     /**
-     * Broadcast a list of cell broadcast messages.
-     * @param cbMessages a list of cell broadcast message.
-     * @param cbMessageUris the corresponding {@link Uri} of the cell broadcast messages.
-     */
-    protected void broadcastMessage(List<SmsCbMessage> cbMessages, List<Uri> cbMessageUris,
-            int slotIndex) {
-        for (int i = 0; i < cbMessages.size(); i++) {
-            broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
-        }
-    }
-
-    /**
      * Broadcast the {@code message} to the applications.
      * @param message a message need to broadcast
      * @param messageUri message's uri
@@ -275,11 +507,34 @@
         if (messageUri != null) {
             ContentValues cv = new ContentValues();
             cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1);
-            mContext.getContentResolver().update(CELL_BROADCAST_URI, cv,
+            mContext.getContentResolver().update(CellBroadcasts.CONTENT_URI, cv,
                     CellBroadcasts._ID + "=?", new String[] {messageUri.getLastPathSegment()});
         }
     }
 
+    /**
+     * Get the device resource based on SIM
+     *
+     * @param subId Subscription index
+     *
+     * @return The resource
+     */
+    public @NonNull Resources getResources(int subId) {
+        if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID
+                || !SubscriptionManager.isValidSubscriptionId(subId)) {
+            return mContext.getResources();
+        }
+
+        if (mResourcesCache.containsKey(subId)) {
+            return mResourcesCache.get(subId);
+        }
+
+        Resources res = SubscriptionManager.getResourcesForSubId(mContext, subId);
+        mResourcesCache.put(subId, res);
+
+        return res;
+    }
+
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("CellBroadcastHandler:");
@@ -398,8 +653,8 @@
         }
 
         private boolean hasPermission(String permission) {
-            return PermissionChecker.checkCallingOrSelfPermissionForDataDelivery(mContext,
-                    permission) == PERMISSION_GRANTED;
+            return mContext.checkPermission(permission, Process.myPid(), Process.myUid())
+                    == PackageManager.PERMISSION_GRANTED;
         }
 
         private final LocationListener mLocationListener = new LocationListener() {
diff --git a/src/com/android/cellbroadcastservice/CellBroadcastProvider.java b/src/com/android/cellbroadcastservice/CellBroadcastProvider.java
new file mode 100644
index 0000000..8b1a747
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/CellBroadcastProvider.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2019 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 com.android.cellbroadcastservice;
+
+import android.app.AppOpsManager;
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Process;
+import android.provider.Telephony.CellBroadcasts;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+
+/**
+ * The content provider that provides access of cell broadcast message to application.
+ * Permission {@link android.permission.READ_CELL_BROADCASTS} is required for querying the cell
+ * broadcast message. Only phone process has the permission to write/update the database via this
+ * provider.
+ */
+public class CellBroadcastProvider extends ContentProvider {
+    /** Interface for read/write permission check. */
+    public interface PermissionChecker {
+        /** Return {@code True} if the caller has the permission to write/update the database. */
+        boolean hasWritePermission();
+
+        /** Return {@code True} if the caller has the permission to query the complete database. */
+        boolean hasReadPermission();
+
+        /**
+         * Return {@code True} if the caller has the permission to query the database for
+         * cell broadcast message history.
+         */
+        boolean hasReadPermissionForHistory();
+    }
+
+    private static final String TAG = CellBroadcastProvider.class.getSimpleName();
+
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+
+    /** Database name. */
+    private static final String DATABASE_NAME = "cellbroadcasts.db";
+
+    /** Database version. */
+    private static final int DATABASE_VERSION = 2;
+
+    /** URI matcher for ContentProvider queries. */
+    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    /** URI matcher type to get all cell broadcasts. */
+    private static final int ALL = 0;
+
+    /**
+     * URI matcher type for get all message history, this is used primarily for default
+     * cellbroadcast app or messaging app to display message history. some information is not
+     * exposed for messaging history, e.g, messages which are out of broadcast geometrics will not
+     * be delivered to end users thus will not be returned as message history query result.
+     */
+    private static final int MESSAGE_HISTORY = 1;
+
+    /** MIME type for the list of all cell broadcasts. */
+    private static final String LIST_TYPE = "vnd.android.cursor.dir/cellbroadcast";
+
+    /** Table name of cell broadcast message. */
+    @VisibleForTesting
+    public static final String CELL_BROADCASTS_TABLE_NAME = "cell_broadcasts";
+
+    /** Authority string for content URIs. */
+    @VisibleForTesting
+    public static final String AUTHORITY = "cellbroadcasts";
+
+    /** Content uri of this provider. */
+    public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
+
+    @VisibleForTesting
+    public PermissionChecker mPermissionChecker;
+
+    /** The database helper for this content provider. */
+    @VisibleForTesting
+    public SQLiteOpenHelper mDbHelper;
+
+    static {
+        sUriMatcher.addURI(AUTHORITY, null, ALL);
+        sUriMatcher.addURI(AUTHORITY, "history", MESSAGE_HISTORY);
+    }
+
+    public CellBroadcastProvider() {}
+
+    @VisibleForTesting
+    public CellBroadcastProvider(PermissionChecker permissionChecker) {
+        mPermissionChecker = permissionChecker;
+    }
+
+    @Override
+    public boolean onCreate() {
+        mDbHelper = new CellBroadcastDatabaseHelper(getContext());
+        mPermissionChecker = new CellBroadcastPermissionChecker();
+        setAppOps(AppOpsManager.OP_READ_CELL_BROADCASTS, AppOpsManager.OP_NONE);
+        return true;
+    }
+
+    /**
+     * Return the MIME type of the data at the specified URI.
+     *
+     * @param uri the URI to query.
+     * @return a MIME type string, or null if there is no type.
+     */
+    @Override
+    public String getType(Uri uri) {
+        int match = sUriMatcher.match(uri);
+        switch (match) {
+            case ALL:
+                return LIST_TYPE;
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        checkReadPermission(uri);
+
+        if (DBG) {
+            Log.d(TAG, "query:"
+                    + " uri = " + uri
+                    + " projection = " + Arrays.toString(projection)
+                    + " selection = " + selection
+                    + " selectionArgs = " + Arrays.toString(selectionArgs)
+                    + " sortOrder = " + sortOrder);
+        }
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setStrict(true); // a little protection from injection attacks
+        qb.setTables(CELL_BROADCASTS_TABLE_NAME);
+
+        String orderBy;
+        if (!TextUtils.isEmpty(sortOrder)) {
+            orderBy = sortOrder;
+        } else {
+            orderBy = CellBroadcasts.RECEIVED_TIME + " DESC";
+        }
+
+        int match = sUriMatcher.match(uri);
+        switch (match) {
+            case ALL:
+                return getReadableDatabase().query(
+                        CELL_BROADCASTS_TABLE_NAME, projection, selection, selectionArgs,
+                        null /* groupBy */, null /* having */, orderBy);
+            case MESSAGE_HISTORY:
+                // limit projections to certain columns. limit result to broadcasted messages only.
+                qb.appendWhere(CellBroadcasts.MESSAGE_BROADCASTED  + "=1");
+                return qb.query(getReadableDatabase(), projection, selection, selectionArgs, null,
+                        null, orderBy);
+            default:
+                throw new IllegalArgumentException(
+                        "Query method doesn't support this uri = " + uri);
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        checkWritePermission();
+
+        if (DBG) {
+            Log.d(TAG, "insert:"
+                    + " uri = " + uri
+                    + " contentValue = " + values);
+        }
+
+        switch (sUriMatcher.match(uri)) {
+            case ALL:
+                long row = getWritableDatabase().insertOrThrow(CELL_BROADCASTS_TABLE_NAME, null,
+                        values);
+                if (row > 0) {
+                    Uri newUri = ContentUris.withAppendedId(CONTENT_URI, row);
+                    getContext().getContentResolver()
+                            .notifyChange(CONTENT_URI, null /* observer */);
+                    return newUri;
+                } else {
+                    Log.e(TAG, "Insert record failed because of unknown reason, uri = " + uri);
+                    return null;
+                }
+            default:
+                throw new IllegalArgumentException(
+                        "Insert method doesn't support this uri = " + uri);
+        }
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        checkWritePermission();
+
+        if (DBG) {
+            Log.d(TAG, "delete:"
+                    + " uri = " + uri
+                    + " selection = " + selection
+                    + " selectionArgs = " + Arrays.toString(selectionArgs));
+        }
+
+        switch (sUriMatcher.match(uri)) {
+            case ALL:
+                return getWritableDatabase().delete(CELL_BROADCASTS_TABLE_NAME,
+                        selection, selectionArgs);
+            default:
+                throw new IllegalArgumentException(
+                        "Delete method doesn't support this uri = " + uri);
+        }
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        checkWritePermission();
+
+        if (DBG) {
+            Log.d(TAG, "update:"
+                    + " uri = " + uri
+                    + " values = {" + values + "}"
+                    + " selection = " + selection
+                    + " selectionArgs = " + Arrays.toString(selectionArgs));
+        }
+
+        switch (sUriMatcher.match(uri)) {
+            case ALL:
+                int rowCount = getWritableDatabase().update(
+                        CELL_BROADCASTS_TABLE_NAME,
+                        values,
+                        selection,
+                        selectionArgs);
+                if (rowCount > 0) {
+                    getContext().getContentResolver().notifyChange(uri, null /* observer */);
+                }
+                return rowCount;
+            default:
+                throw new IllegalArgumentException(
+                        "Update method doesn't support this uri = " + uri);
+        }
+    }
+
+    /**
+     * Returns a string used to create the cell broadcast table. This is exposed so the unit test
+     * can construct its own in-memory database to match the cell broadcast db.
+     */
+    @VisibleForTesting
+    public static String getStringForCellBroadcastTableCreation(String tableName) {
+        return "CREATE TABLE " + tableName + " ("
+                + CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+                + CellBroadcasts.SUB_ID + " INTEGER,"
+                + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0,"
+                + CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER,"
+                + CellBroadcasts.PLMN + " TEXT,"
+                + CellBroadcasts.LAC + " INTEGER,"
+                + CellBroadcasts.CID + " INTEGER,"
+                + CellBroadcasts.SERIAL_NUMBER + " INTEGER,"
+                + CellBroadcasts.SERVICE_CATEGORY + " INTEGER,"
+                + CellBroadcasts.LANGUAGE_CODE + " TEXT,"
+                + CellBroadcasts.MESSAGE_BODY + " TEXT,"
+                + CellBroadcasts.MESSAGE_FORMAT + " INTEGER,"
+                + CellBroadcasts.MESSAGE_PRIORITY + " INTEGER,"
+                + CellBroadcasts.ETWS_WARNING_TYPE + " INTEGER,"
+                + CellBroadcasts.CMAS_MESSAGE_CLASS + " INTEGER,"
+                + CellBroadcasts.CMAS_CATEGORY + " INTEGER,"
+                + CellBroadcasts.CMAS_RESPONSE_TYPE + " INTEGER,"
+                + CellBroadcasts.CMAS_SEVERITY + " INTEGER,"
+                + CellBroadcasts.CMAS_URGENCY + " INTEGER,"
+                + CellBroadcasts.CMAS_CERTAINTY + " INTEGER,"
+                + CellBroadcasts.RECEIVED_TIME + " BIGINT,"
+                + CellBroadcasts.MESSAGE_BROADCASTED + " BOOLEAN DEFAULT 0,"
+                + CellBroadcasts.GEOMETRIES + " TEXT,"
+                + CellBroadcasts.MAXIMUM_WAIT_TIME + " INTEGER);";
+    }
+
+    private SQLiteDatabase getWritableDatabase() {
+        return mDbHelper.getWritableDatabase();
+    }
+
+    private SQLiteDatabase getReadableDatabase() {
+        return mDbHelper.getReadableDatabase();
+    }
+
+    private void checkWritePermission() {
+        if (!mPermissionChecker.hasWritePermission()) {
+            throw new SecurityException(
+                    "No permission to write CellBroadcast provider");
+        }
+    }
+
+    private void checkReadPermission(Uri uri) {
+        int match = sUriMatcher.match(uri);
+        switch (match) {
+            case ALL:
+                if (!mPermissionChecker.hasReadPermission()) {
+                    throw new SecurityException(
+                            "No permission to read CellBroadcast provider");
+                }
+                break;
+            case MESSAGE_HISTORY:
+                // TODO: if we plan to allow apps to query db in framework, we should migrate data
+                // first before deprecating app's database. otherwise users will lose all history.
+                if (!mPermissionChecker.hasReadPermissionForHistory()) {
+                    throw new SecurityException(
+                            "No permission to read CellBroadcast provider for message history");
+                }
+                break;
+            default:
+                return;
+        }
+    }
+
+    private class CellBroadcastDatabaseHelper extends SQLiteOpenHelper {
+        CellBroadcastDatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (DBG) {
+                Log.d(TAG, "onUpgrade: oldV=" + oldVersion + " newV=" + newVersion);
+            }
+            if (newVersion == 2) {
+                db.execSQL("ALTER TABLE " + CELL_BROADCASTS_TABLE_NAME + " ADD COLUMN "
+                        + CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;");
+                Log.d(TAG, "add slotIndex column");
+            }
+        }
+    }
+
+    private class CellBroadcastPermissionChecker implements PermissionChecker {
+        @Override
+        public boolean hasWritePermission() {
+            // Only the phone and network statck process has the write permission to modify this
+            // provider.
+            return Binder.getCallingUid() == Process.PHONE_UID
+                    || Binder.getCallingUid() == Process.NETWORK_STACK_UID;
+        }
+
+        @Override
+        public boolean hasReadPermission() {
+            // Only the phone and network stack process has the read permission to query data from
+            // this provider.
+            return Binder.getCallingUid() == Process.PHONE_UID
+                    || Binder.getCallingUid() == Process.NETWORK_STACK_UID;
+        }
+
+        @Override
+        public boolean hasReadPermissionForHistory() {
+            int status = getContext().checkCallingOrSelfPermission(
+                    "android.permission.RECEIVE_EMERGENCY_BROADCAST");
+            if (status == PackageManager.PERMISSION_GRANTED) {
+                return true;
+            }
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
index abcbc20..1a5ed14 100644
--- a/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
+++ b/src/com/android/cellbroadcastservice/GsmCellBroadcastHandler.java
@@ -110,7 +110,7 @@
 
         ContentResolver resolver = mContext.getContentResolver();
         for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) {
-            try (Cursor cursor = resolver.query(CELL_BROADCAST_URI,
+            try (Cursor cursor = resolver.query(CellBroadcasts.CONTENT_URI,
                     CellBroadcasts.QUERY_COLUMNS_FWK,
                     where,
                     new String[] { Integer.toString(identity.messageIdentifier),
@@ -120,7 +120,7 @@
                 if (cursor != null) {
                     while (cursor.moveToNext()) {
                         cbMessages.add(SmsCbMessage.createFromCursor(cursor));
-                        cbMessageUris.add(ContentUris.withAppendedId(CELL_BROADCAST_URI,
+                        cbMessageUris.add(ContentUris.withAppendedId(CellBroadcasts.CONTENT_URI,
                                 cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID))));
                     }
                 }
@@ -159,7 +159,9 @@
         requestLocationUpdate(location -> {
             if (location == null) {
                 // If the location is not available, broadcast the messages directly.
-                broadcastMessage(cbMessages, cbMessageUris, slotIndex);
+                for (int i = 0; i < cbMessages.size(); i++) {
+                    broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex);
+                }
             } else {
                 for (int i = 0; i < cbMessages.size(); i++) {
                     List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty()
@@ -200,6 +202,9 @@
             } else {
                 SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex);
                 if (cbMessage != null) {
+                    if (isDuplicate(cbMessage)) {
+                        return false;
+                    }
                     handleBroadcastSms(cbMessage);
                     return true;
                 }
diff --git a/src/com/android/cellbroadcastservice/SmsCbConstants.java b/src/com/android/cellbroadcastservice/SmsCbConstants.java
new file mode 100644
index 0000000..9a2765b
--- /dev/null
+++ b/src/com/android/cellbroadcastservice/SmsCbConstants.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2019 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 com.android.cellbroadcastservice;
+
+/**
+ * Constants used in SMS Cell Broadcast messages (see 3GPP TS 23.041).
+ */
+public class SmsCbConstants {
+
+    /** Private constructor for utility class. */
+    private SmsCbConstants() { }
+
+    /** Start of PWS Message Identifier range (includes ETWS and CMAS). */
+    public static final int MESSAGE_ID_PWS_FIRST_IDENTIFIER =
+            0x1100; // 4352
+
+    /** Bitmask for messages of ETWS type (including future extensions). */
+    public static final int MESSAGE_ID_ETWS_TYPE_MASK =
+            0xFFF8;
+
+    /** Value for messages of ETWS type after applying {@link #MESSAGE_ID_ETWS_TYPE_MASK}. */
+    public static final int MESSAGE_ID_ETWS_TYPE =
+            0x1100; // 4352
+
+    /** ETWS Message Identifier for earthquake warning message. */
+    public static final int MESSAGE_ID_ETWS_EARTHQUAKE_WARNING =
+            0x1100; // 4352
+
+    /** ETWS Message Identifier for tsunami warning message. */
+    public static final int MESSAGE_ID_ETWS_TSUNAMI_WARNING =
+            0x1101; // 4353
+
+    /** ETWS Message Identifier for earthquake and tsunami combined warning message. */
+    public static final int MESSAGE_ID_ETWS_EARTHQUAKE_AND_TSUNAMI_WARNING =
+            0x1102; // 4354
+
+    /** ETWS Message Identifier for test message. */
+    public static final int MESSAGE_ID_ETWS_TEST_MESSAGE =
+            0x1103; // 4355
+
+    /** ETWS Message Identifier for messages related to other emergency types. */
+    public static final int MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE =
+            0x1104; // 4356
+
+    /** Start of CMAS Message Identifier range. */
+    public static final int MESSAGE_ID_CMAS_FIRST_IDENTIFIER =
+            0x1112; // 4370
+
+    /** CMAS Message Identifier for Presidential Level alerts. */
+    public static final int MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL =
+            0x1112; // 4370
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Observed. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED =
+            0x1113; // 4371
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Likely. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY =
+            0x1114; // 4372
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Observed. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED =
+            0x1115; // 4373
+
+    /** CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Likely. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY =
+            0x1116; // 4374
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Observed. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED =
+            0x1117; // 4375
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Likely. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY =
+            0x1118; // 4376
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Observed. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED =
+            0x1119; // 4377
+
+    /** CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Likely. */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY =
+            0x111A; // 4378
+
+    /** CMAS Message Identifier for Child Abduction Emergency (Amber Alert). */
+    public static final int MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY =
+            0x111B; // 4379
+
+    /** CMAS Message Identifier for the Required Monthly Test. */
+    public static final int MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST =
+            0x111C; // 4380
+
+    /** CMAS Message Identifier for CMAS Exercise. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXERCISE =
+            0x111D; // 4381
+
+    /** CMAS Message Identifier for operator defined use. */
+    public static final int MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE =
+            0x111E; // 4382
+
+    /** CMAS Message Identifier for Presidential Level alerts for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE =
+            0x111F; // 4383
+
+    /**
+     * CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Observed
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE =
+            0x1120; // 4384
+
+    /**
+     * CMAS Message Identifier for Extreme alerts, Urgency=Immediate, Certainty=Likely
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE =
+            0x1121; // 4385
+
+    /**
+     * CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Observed
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE =
+            0x1122; // 4386
+
+    /**
+     * CMAS Message Identifier for Extreme alerts, Urgency=Expected, Certainty=Likely
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE =
+            0x1123; // 4387
+
+    /**
+     * CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Observed
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE =
+            0x1124; // 4388
+
+    /**
+     * CMAS Message Identifier for Severe alerts, Urgency=Immediate, Certainty=Likely
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE =
+            0x1125; // 4389
+
+    /**
+     * CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Observed
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE =
+            0x1126; // 4390
+
+    /**
+     * CMAS Message Identifier for Severe alerts, Urgency=Expected, Certainty=Likely
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE =
+            0x1127; // 4391
+
+    /**
+     * CMAS Message Identifier for Child Abduction Emergency (Amber Alert)
+     * for additional languages.
+     */
+    public static final int MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE =
+            0x1128; // 4392
+
+    /** CMAS Message Identifier for the Required Monthly Test  for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE =
+            0x1129; // 4393
+
+    /** CMAS Message Identifier for CMAS Exercise for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_EXERCISE_LANGUAGE =
+            0x112A; // 4394
+
+    /** CMAS Message Identifier for operator defined use for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE_LANGUAGE =
+            0x112B; // 4395
+
+    /** CMAS Message Identifier for CMAS Public Safety Alerts. */
+    public static final int MESSAGE_ID_CMAS_ALERT_PUBLIC_SAFETY =
+            0x112C; // 4396
+
+    /** CMAS Message Identifier for CMAS Public Safety Alerts for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_PUBLIC_SAFETY_LANGUAGE =
+            0x112D; // 4397
+
+    /** CMAS Message Identifier for CMAS State/Local Test. */
+    public static final int MESSAGE_ID_CMAS_ALERT_STATE_LOCAL_TEST =
+            0x112E; // 4398
+
+    /** CMAS Message Identifier for CMAS State/Local Test for additional languages. */
+    public static final int MESSAGE_ID_CMAS_ALERT_STATE_LOCAL_TEST_LANGUAGE =
+            0x112F; // 4399
+
+    /** CMAS Message Identifier for CMAS geo fencing trigger message. */
+    public static final int MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER =
+            0x1130; // 4400
+
+    /** End of CMAS Message Identifier range. */
+    public static final int MESSAGE_ID_CMAS_LAST_IDENTIFIER = MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER;
+
+    /** End of PWS Message Identifier range (includes ETWS, CMAS, and future extensions). */
+    public static final int MESSAGE_ID_PWS_LAST_IDENTIFIER =
+            0x18FF; // 6399
+
+    /** ETWS serial number flag to activate the popup display. */
+    public static final int SERIAL_NUMBER_ETWS_ACTIVATE_POPUP =
+            0x1000; // 4096
+
+    /** ETWS serial number flag to activate the emergency user alert. */
+    public static final int SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT =
+            0x2000; // 8192
+}
diff --git a/src/com/android/cellbroadcastservice/SmsCbHeader.java b/src/com/android/cellbroadcastservice/SmsCbHeader.java
index a19a0ed..2a859fa 100644
--- a/src/com/android/cellbroadcastservice/SmsCbHeader.java
+++ b/src/com/android/cellbroadcastservice/SmsCbHeader.java
@@ -20,7 +20,6 @@
 import android.telephony.SmsCbEtwsInfo;
 
 import com.android.internal.telephony.SmsConstants;
-import com.android.internal.telephony.gsm.SmsCbConstants;
 
 import dalvik.annotation.compat.UnsupportedAppUsage;
 
diff --git a/src/com/android/cellbroadcastservice/WakeLockStateMachine.java b/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
index 99b935c..7de49f0 100644
--- a/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
+++ b/src/com/android/cellbroadcastservice/WakeLockStateMachine.java
@@ -51,6 +51,9 @@
     /** Release wakelock after a short timeout when returning to idle state. */
     static final int EVENT_RELEASE_WAKE_LOCK = 3;
 
+    /** Broadcast not required due to geo-fencing check */
+    static final int EVENT_BROADCAST_NOT_REQUIRED = 4;
+
     @UnsupportedAppUsage
     protected Context mContext;
 
@@ -148,13 +151,14 @@
         @Override
         public void exit() {
             mWakeLock.acquire();
-            if (DBG) log("acquired wakelock, leaving Idle state");
+            if (DBG) log("Idle: acquired wakelock, leaving Idle state");
         }
 
         @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
                 case EVENT_NEW_SMS_MESSAGE:
+                    log("Idle: new cell broadcast message");
                     // transition to waiting state if we sent a broadcast
                     if (handleSmsMessage(msg)) {
                         transitionTo(mWaitingState);
@@ -162,9 +166,12 @@
                     return HANDLED;
 
                 case EVENT_RELEASE_WAKE_LOCK:
+                    log("Idle: release wakelock");
                     releaseWakeLock();
                     return HANDLED;
-
+                case EVENT_BROADCAST_NOT_REQUIRED:
+                    log("Idle: broadcast not required");
+                    return HANDLED;
                 default:
                     return NOT_HANDLED;
             }
@@ -180,19 +187,25 @@
         public boolean processMessage(Message msg) {
             switch (msg.what) {
                 case EVENT_NEW_SMS_MESSAGE:
-                    log("deferring message until return to idle");
+                    log("Waiting: deferring message until return to idle");
                     deferMessage(msg);
                     return HANDLED;
 
                 case EVENT_BROADCAST_COMPLETE:
-                    log("broadcast complete, returning to idle");
+                    log("Waiting: broadcast complete, returning to idle");
                     transitionTo(mIdleState);
                     return HANDLED;
 
                 case EVENT_RELEASE_WAKE_LOCK:
+                    log("Waiting: release wakelock");
                     releaseWakeLock();
                     return HANDLED;
-
+                case EVENT_BROADCAST_NOT_REQUIRED:
+                    log("Waiting: broadcast not required");
+                    if (mReceiverCount.get() == 0) {
+                        transitionTo(mIdleState);
+                    }
+                    return HANDLED;
                 default:
                     return NOT_HANDLED;
             }
diff --git a/tests/Android.bp b/tests/Android.bp
new file mode 100644
index 0000000..f59dde6
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,21 @@
+android_test {
+    name: "CellBroadcastServiceTests",
+    static_libs: [
+        "mockito-target",
+        "compatibility-device-util-axt",
+        "androidx.test.rules",
+        "truth-prebuilt",
+        "testables",
+    ],
+    libs: [
+        "android.test.runner",
+        "telephony-common",
+        "android.test.base",
+        "android.test.mock",
+    ],
+    srcs: ["src/**/*.java", ":cellbroadcast-shared-srcs"],
+    platform_apis: true,
+    test_suites: ["device-tests"],
+    certificate: "platform",
+    instrumentation_for: "CellBroadcastServiceModule",
+}
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..7403d26
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.cellbroadcastservice.tests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.cellbroadcastservice.tests"
+        android:label="Tests for CellBroadcastService">
+    </instrumentation>
+</manifest>
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
new file mode 100644
index 0000000..022ff36
--- /dev/null
+++ b/tests/AndroidTest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Run Tests for CellBroadcastService.">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="CellBroadcastServiceTests.apk" />
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-tag" value="CellBroadcastServiceTests" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.cellbroadcastservice.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/tests/src/com/android/cellbroadcastservice/CellBroadcastHandlerTest.java b/tests/src/com/android/cellbroadcastservice/CellBroadcastHandlerTest.java
new file mode 100644
index 0000000..0eab2c9
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/CellBroadcastHandlerTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 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 com.android.cellbroadcastservice;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+
+import android.content.ContentValues;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.Telephony;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.text.format.DateUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Map;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class CellBroadcastHandlerTest extends CellBroadcastServiceTestBase {
+
+    private CellBroadcastHandler mCellBroadcastHandler;
+
+    @Mock
+    private Map<Integer, Resources> mMockedResourcesCache;
+
+    private class CellBroadcastContentProvider extends MockContentProvider {
+        @Override
+        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+                            String sortOrder) {
+
+            if (uri.compareTo(Telephony.CellBroadcasts.CONTENT_URI) == 0) {
+                MatrixCursor mc = new MatrixCursor(Telephony.CellBroadcasts.QUERY_COLUMNS_FWK);
+
+                mc.addRow(new Object[]{
+                        1,              // _ID
+                        0,              // SLOT_INDEX
+                        0,              // GEOGRAPHICAL_SCOPE
+                        "311480",       // PLMN
+                        0,              // LAC
+                        0,              // CID
+                        1234,           // SERIAL_NUMBER
+                        SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL,
+                        "en",           // LANGUAGE_CODE
+                        "Test Message", // MESSAGE_BODY
+                        1,              // MESSAGE_FORMAT
+                        3,              // MESSAGE_PRIORITY
+                        0,              // ETWS_WARNING_TYPE
+                        SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, // CMAS_MESSAGE_CLASS
+                        0,              // CMAS_CATEGORY
+                        0,              // CMAS_RESPONSE_TYPE
+                        0,              // CMAS_SEVERITY
+                        0,              // CMAS_URGENCY
+                        0,              // CMAS_CERTAINTY
+                        System.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS * 2,
+                        true,           // MESSAGE_BROADCASTED
+                        "",             // GEOMETRIES
+                        5,              // MAXIMUM_WAIT_TIME
+                });
+
+                return mc;
+            }
+
+            return null;
+        }
+
+        @Override
+        public int update(Uri url, ContentValues values, String where, String[] whereArgs) {
+            return 1;
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+        mCellBroadcastHandler = new CellBroadcastHandler("CellBroadcastHandlerUT", mMockedContext);
+        ((MockContentResolver) mMockedContext.getContentResolver()).addProvider(
+                Telephony.CellBroadcasts.CONTENT_URI.getAuthority(),
+                new CellBroadcastContentProvider());
+        doReturn(true).when(mMockedResourcesCache).containsKey(anyInt());
+        doReturn(mMockedResources).when(mMockedResourcesCache).get(anyInt());
+        replaceInstance(CellBroadcastHandler.class, "mResourcesCache", mCellBroadcastHandler,
+                mMockedResourcesCache);
+        putResources(R.integer.message_expiration_time, (int) DateUtils.DAY_IN_MILLIS);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    private SmsCbMessage createSmsCbMessage(int serialNumber, int serviceCategory,
+                                            String messageBody) {
+        return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
+                0, serialNumber, new SmsCbLocation(),
+                serviceCategory, "en", messageBody, 3,
+                null, null, 0);
+    }
+
+    @Test
+    @SmallTest
+    public void testDuplicate() throws Exception {
+        SmsCbMessage msg = createSmsCbMessage(1234, 4370, "msg");
+        assertTrue(mCellBroadcastHandler.isDuplicate(msg));
+    }
+
+    @Test
+    @SmallTest
+    public void testNotDuplicateSerialDifferent() throws Exception {
+        SmsCbMessage msg = createSmsCbMessage(1235, 4370, "msg");
+        assertFalse(mCellBroadcastHandler.isDuplicate(msg));
+    }
+
+    @Test
+    @SmallTest
+    public void testNotDuplicateServiceCategoryDifferent() throws Exception {
+        SmsCbMessage msg = createSmsCbMessage(1234, 4371, "msg");
+        assertFalse(mCellBroadcastHandler.isDuplicate(msg));
+    }
+
+    @Test
+    @SmallTest
+    public void testNotDuplicateMessageBodyDifferent() throws Exception {
+        putResources(R.bool.duplicate_compare_body, true);
+        SmsCbMessage msg = createSmsCbMessage(1234, 4370, "msg");
+        assertFalse(mCellBroadcastHandler.isDuplicate(msg));
+    }
+}
diff --git a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java b/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java
new file mode 100644
index 0000000..dc9837f
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTest.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2019 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 com.android.cellbroadcastservice;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+
+import android.content.ContentValues;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Telephony.CellBroadcasts;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import android.util.Log;
+
+import com.android.cellbroadcastservice.CellBroadcastProvider.PermissionChecker;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class CellBroadcastProviderTest extends TestCase {
+    private static final String TAG = CellBroadcastProviderTest.class.getSimpleName();
+
+    public static final Uri CONTENT_URI = Uri.parse("content://cellbroadcasts");
+
+    private static final int GEO_SCOPE = 1;
+    private static final String PLMN = "123456";
+    private static final int LAC = 13;
+    private static final int CID = 123;
+    private static final int SERIAL_NUMBER = 17984;
+    private static final int SERVICE_CATEGORY = 4379;
+    private static final String LANGUAGE_CODE = "en";
+    private static final String MESSAGE_BODY = "AMBER Alert: xxxx";
+    private static final int MESSAGE_FORMAT = 1;
+    private static final int MESSAGE_PRIORITY = 3;
+    private static final int ETWS_WARNING_TYPE = 1;
+    private static final int CMAS_MESSAGE_CLASS = 1;
+    private static final int CMAS_CATEGORY = 6;
+    private static final int CMAS_RESPONSE_TYPE = 1;
+    private static final int CMAS_SEVERITY = 2;
+    private static final int CMAS_URGENCY = 3;
+    private static final int CMAS_CERTAINTY = 4;
+    private static final int RECEIVED_TIME = 1562792637;
+    private static final int MESSAGE_BROADCASTED = 1;
+    private static final String GEOMETRIES_COORDINATES = "polygon|0,0|0,1|1,1|1,0;circle|0,0|100";
+
+    private static final String SELECT_BY_ID = CellBroadcasts._ID + "=?";
+
+    private static final String[] QUERY_COLUMNS = {
+            CellBroadcasts._ID,
+            CellBroadcasts.GEOGRAPHICAL_SCOPE,
+            CellBroadcasts.PLMN,
+            CellBroadcasts.LAC,
+            CellBroadcasts.CID,
+            CellBroadcasts.SERIAL_NUMBER,
+            CellBroadcasts.SERVICE_CATEGORY,
+            CellBroadcasts.LANGUAGE_CODE,
+            CellBroadcasts.MESSAGE_BODY,
+            CellBroadcasts.MESSAGE_FORMAT,
+            CellBroadcasts.MESSAGE_PRIORITY,
+            CellBroadcasts.ETWS_WARNING_TYPE,
+            CellBroadcasts.CMAS_MESSAGE_CLASS,
+            CellBroadcasts.CMAS_CATEGORY,
+            CellBroadcasts.CMAS_RESPONSE_TYPE,
+            CellBroadcasts.CMAS_SEVERITY,
+            CellBroadcasts.CMAS_URGENCY,
+            CellBroadcasts.CMAS_CERTAINTY,
+            CellBroadcasts.RECEIVED_TIME,
+            CellBroadcasts.MESSAGE_BROADCASTED,
+            CellBroadcasts.GEOMETRIES
+    };
+
+    private CellBroadcastProviderTestable mCellBroadcastProviderTestable;
+    private MockContextWithProvider mContext;
+    private MockContentResolver mContentResolver;
+
+    @Mock
+    private PermissionChecker mMockPermissionChecker;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        doReturn(true).when(mMockPermissionChecker).hasReadPermission();
+        doReturn(true).when(mMockPermissionChecker).hasWritePermission();
+
+        mCellBroadcastProviderTestable = new CellBroadcastProviderTestable(mMockPermissionChecker);
+        mContext = new MockContextWithProvider(mCellBroadcastProviderTestable);
+        mContentResolver = mContext.getContentResolver();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mCellBroadcastProviderTestable.closeDatabase();
+        super.tearDown();
+    }
+
+    @Test
+    public void testUpdate() {
+        // Insert a cellbroadcast to the database.
+        ContentValues cv = fakeCellBroadcast();
+        Uri uri = mContentResolver.insert(CONTENT_URI, cv);
+        assertThat(uri).isNotNull();
+
+        // Change some fields of this cell broadcast.
+        int messageBroadcasted = 1 - cv.getAsInteger(CellBroadcasts.MESSAGE_BROADCASTED);
+        int receivedTime = 1234555555;
+        cv.put(CellBroadcasts.MESSAGE_BROADCASTED, messageBroadcasted);
+        cv.put(CellBroadcasts.RECEIVED_TIME, receivedTime);
+        mContentResolver.update(CONTENT_URI, cv, SELECT_BY_ID,
+                new String[] { uri.getLastPathSegment() });
+
+        // Query and check if the update is successed.
+        Cursor cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS,
+                SELECT_BY_ID, new String[] { uri.getLastPathSegment() }, null /* orderBy */);
+        cursor.moveToNext();
+
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.RECEIVED_TIME)))
+                .isEqualTo(receivedTime);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_BROADCASTED)))
+                .isEqualTo(messageBroadcasted);
+        cursor.close();
+    }
+
+    @Test
+    public void testUpdate_WithoutWritePermission_fail() {
+        ContentValues cv = fakeCellBroadcast();
+        Uri uri = mContentResolver.insert(CONTENT_URI, cv);
+        assertThat(uri).isNotNull();
+
+        // Revoke the write permission
+        doReturn(false).when(mMockPermissionChecker).hasWritePermission();
+
+        try {
+            mContentResolver.update(CONTENT_URI, cv, SELECT_BY_ID,
+                    new String[] { uri.getLastPathSegment() });
+            fail();
+        } catch (SecurityException ex) {
+            // pass the test
+        }
+    }
+
+    @Test
+    public void testGetAllCellBroadcast() {
+        // Insert some cell broadcasts which message_broadcasted is false
+        int messageNotBroadcastedCount = 5;
+        ContentValues cv = fakeCellBroadcast();
+        cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 0);
+        for (int i = 0; i < messageNotBroadcastedCount; i++) {
+            mContentResolver.insert(CONTENT_URI, cv);
+        }
+
+        // Insert some cell broadcasts which message_broadcasted is true
+        int messageBroadcastedCount = 6;
+        cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1);
+        for (int i = 0; i < messageBroadcastedCount; i++) {
+            mContentResolver.insert(CONTENT_URI, cv);
+        }
+
+        // Query the broadcast with message_broadcasted is false
+        Cursor cursor = mContentResolver.query(
+                CONTENT_URI,
+                QUERY_COLUMNS,
+                String.format("%s=?", CellBroadcasts.MESSAGE_BROADCASTED), /* selection */
+                new String[] {"0"}, /* selectionArgs */
+                null /* sortOrder */);
+        assertThat(cursor.getCount()).isEqualTo(messageNotBroadcastedCount);
+    }
+
+    @Test
+    public void testDelete_withoutWritePermission_throwSecurityException() {
+        Uri uri = mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+        assertThat(uri).isNotNull();
+
+        // Revoke the write permission
+        doReturn(false).when(mMockPermissionChecker).hasWritePermission();
+
+        try {
+            mContentResolver.delete(CONTENT_URI, SELECT_BY_ID,
+                    new String[] { uri.getLastPathSegment() });
+            fail();
+        } catch (SecurityException ex) {
+            // pass the test
+        }
+    }
+
+
+    @Test
+    public void testDelete_oneRecord_success() {
+        // Insert a cellbroadcast to the database.
+        ContentValues cv = fakeCellBroadcast();
+        Uri uri = mContentResolver.insert(CONTENT_URI, cv);
+        assertThat(uri).isNotNull();
+
+        String[] selectionArgs = new String[] { uri.getLastPathSegment() };
+
+        // Ensure the cell broadcast is inserted.
+        Cursor cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS,
+                SELECT_BY_ID, selectionArgs, null /* orderBy */);
+        assertThat(cursor.getCount()).isEqualTo(1);
+        cursor.close();
+
+        // Delete the cell broadcast
+        int rowCount = mContentResolver.delete(CONTENT_URI, SELECT_BY_ID,
+                selectionArgs);
+        assertThat(rowCount).isEqualTo(1);
+
+        // Ensure the cell broadcast is deleted.
+        cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS, SELECT_BY_ID,
+                selectionArgs, null /* orderBy */);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+    }
+
+    @Test
+    public void testDelete_all_success() {
+        // Insert a cellbroadcast to the database.
+        mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+        mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+
+        // Ensure the cell broadcast are inserted.
+        Cursor cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS,
+                null /* selection */, null /* selectionArgs */, null /* orderBy */);
+        assertThat(cursor.getCount()).isEqualTo(2);
+        cursor.close();
+
+        // Delete all cell broadcasts.
+        int rowCount = mContentResolver.delete(
+                CONTENT_URI, null /* selection */, null /* selectionArgs */);
+        assertThat(rowCount).isEqualTo(2);
+        cursor.close();
+
+        // Ensure all cell broadcasts are deleted.
+        cursor = mContentResolver.query(CONTENT_URI, QUERY_COLUMNS,
+                null /* selection */, null /* selectionArgs */, null /* orderBy */);
+        assertThat(cursor.getCount()).isEqualTo(0);
+        cursor.close();
+    }
+
+    @Test
+    public void testInsert_withoutWritePermission_fail() {
+        doReturn(false).when(mMockPermissionChecker).hasWritePermission();
+
+        try {
+            mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+            fail();
+        } catch (SecurityException ex) {
+            // pass the test
+        }
+    }
+
+    @Test
+    public void testInsertAndQuery() {
+        // Insert a cell broadcast message
+        Uri uri = mContentResolver.insert(CONTENT_URI, fakeCellBroadcast());
+
+        // Verify that the return uri is not null and the record is inserted into the database
+        // correctly.
+        assertThat(uri).isNotNull();
+        Cursor cursor = mContentResolver.query(
+                CONTENT_URI, QUERY_COLUMNS, SELECT_BY_ID,
+                new String[] { uri.getLastPathSegment() }, null /* orderBy */);
+        assertThat(cursor.getCount()).isEqualTo(1);
+
+        cursor.moveToNext();
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.GEOGRAPHICAL_SCOPE)))
+                .isEqualTo(GEO_SCOPE);
+        assertThat(cursor.getString(cursor.getColumnIndexOrThrow(CellBroadcasts.PLMN)))
+                .isEqualTo(PLMN);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.LAC))).isEqualTo(LAC);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CID))).isEqualTo(CID);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.SERIAL_NUMBER)))
+                .isEqualTo(SERIAL_NUMBER);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.SERVICE_CATEGORY)))
+                .isEqualTo(SERVICE_CATEGORY);
+        assertThat(cursor.getString(cursor.getColumnIndexOrThrow(CellBroadcasts.LANGUAGE_CODE)))
+                .isEqualTo(LANGUAGE_CODE);
+        assertThat(cursor.getString(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_BODY)))
+                .isEqualTo(MESSAGE_BODY);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_FORMAT)))
+                .isEqualTo(MESSAGE_FORMAT);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_PRIORITY)))
+                .isEqualTo(MESSAGE_PRIORITY);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.ETWS_WARNING_TYPE)))
+                .isEqualTo(ETWS_WARNING_TYPE);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_MESSAGE_CLASS)))
+                .isEqualTo(CMAS_MESSAGE_CLASS);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_CATEGORY)))
+                .isEqualTo(CMAS_CATEGORY);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_RESPONSE_TYPE)))
+                .isEqualTo(CMAS_RESPONSE_TYPE);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_SEVERITY)))
+                .isEqualTo(CMAS_SEVERITY);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_URGENCY)))
+                .isEqualTo(CMAS_URGENCY);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.CMAS_CERTAINTY)))
+                .isEqualTo(CMAS_CERTAINTY);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.RECEIVED_TIME)))
+                .isEqualTo(RECEIVED_TIME);
+        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_BROADCASTED)))
+                .isEqualTo(MESSAGE_BROADCASTED);
+        assertThat(cursor.getString(cursor.getColumnIndexOrThrow(
+                CellBroadcasts.GEOMETRIES))).isEqualTo(GEOMETRIES_COORDINATES);
+    }
+
+    /**
+     * This is used to give the CellBroadcastProviderTest a mocked context which takes a
+     * CellBroadcastProvider and attaches it to the ContentResolver.
+     */
+    private class MockContextWithProvider extends MockContext {
+        private final MockContentResolver mResolver;
+
+        MockContextWithProvider(CellBroadcastProviderTestable cellBroadcastProvider) {
+            mResolver = new MockContentResolver();
+            cellBroadcastProvider.initializeForTesting(this);
+
+            // Add given cellBroadcastProvider to mResolver, so that mResolver can send queries
+            // to the provider.
+            mResolver.addProvider(CellBroadcastProvider.AUTHORITY, cellBroadcastProvider);
+        }
+
+        @Override
+        public MockContentResolver getContentResolver() {
+            return mResolver;
+        }
+
+
+        @Override
+        public Object getSystemService(String name) {
+            Log.d(TAG, "getSystemService: returning null");
+            return null;
+        }
+
+        @Override
+        public int checkCallingOrSelfPermission(String permission) {
+            return PackageManager.PERMISSION_GRANTED;
+        }
+    }
+
+    private static ContentValues fakeCellBroadcast() {
+        ContentValues cv = new ContentValues();
+        cv.put(CellBroadcasts.GEOGRAPHICAL_SCOPE, GEO_SCOPE);
+        cv.put(CellBroadcasts.PLMN, PLMN);
+        cv.put(CellBroadcasts.LAC, LAC);
+        cv.put(CellBroadcasts.CID, CID);
+        cv.put(CellBroadcasts.SERIAL_NUMBER, SERIAL_NUMBER);
+        cv.put(CellBroadcasts.SERVICE_CATEGORY, SERVICE_CATEGORY);
+        cv.put(CellBroadcasts.LANGUAGE_CODE, LANGUAGE_CODE);
+        cv.put(CellBroadcasts.MESSAGE_BODY, MESSAGE_BODY);
+        cv.put(CellBroadcasts.MESSAGE_FORMAT, MESSAGE_FORMAT);
+        cv.put(CellBroadcasts.MESSAGE_PRIORITY, MESSAGE_PRIORITY);
+        cv.put(CellBroadcasts.ETWS_WARNING_TYPE, ETWS_WARNING_TYPE);
+        cv.put(CellBroadcasts.CMAS_MESSAGE_CLASS, CMAS_MESSAGE_CLASS);
+        cv.put(CellBroadcasts.CMAS_CATEGORY, CMAS_CATEGORY);
+        cv.put(CellBroadcasts.CMAS_RESPONSE_TYPE, CMAS_RESPONSE_TYPE);
+        cv.put(CellBroadcasts.CMAS_SEVERITY, CMAS_SEVERITY);
+        cv.put(CellBroadcasts.CMAS_URGENCY, CMAS_URGENCY);
+        cv.put(CellBroadcasts.CMAS_CERTAINTY, CMAS_CERTAINTY);
+        cv.put(CellBroadcasts.RECEIVED_TIME, RECEIVED_TIME);
+        cv.put(CellBroadcasts.MESSAGE_BROADCASTED, MESSAGE_BROADCASTED);
+        cv.put(CellBroadcasts.GEOMETRIES, GEOMETRIES_COORDINATES);
+        return cv;
+    }
+}
diff --git a/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java b/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java
new file mode 100644
index 0000000..2be7512
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/CellBroadcastProviderTestable.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2019 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 com.android.cellbroadcastservice;
+
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+public class CellBroadcastProviderTestable extends CellBroadcastProvider {
+    private static final String TAG = CellBroadcastProviderTestable.class.getSimpleName();
+
+    public CellBroadcastProviderTestable(PermissionChecker permissionChecker) {
+        super(permissionChecker);
+    }
+
+    @Override
+    public boolean onCreate() {
+        // DO NOT call super.onCreate(), otherwise the permission checker will be override.
+        Log.d(TAG, "CellBroadcastProviderTestable onCreate");
+        mDbHelper = new InMemoryCellBroadcastProviderDbHelper();
+        return true;
+    }
+
+    public void closeDatabase() {
+        mDbHelper.close();
+    }
+
+    public static class InMemoryCellBroadcastProviderDbHelper extends SQLiteOpenHelper {
+        public InMemoryCellBroadcastProviderDbHelper() {
+            super(InstrumentationRegistry.getTargetContext(),
+                    null,    // db file name is null for in-memory db
+                    null,    // CursorFactory is null by default
+                    1);      // db version is no-op for tests
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            Log.d(TAG, "IN MEMORY DB CREATED");
+            db.execSQL(getStringForCellBroadcastTableCreation(CELL_BROADCASTS_TABLE_NAME));
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
+    }
+
+    public void initializeForTesting(Context context) {
+        ProviderInfo providerInfo = new ProviderInfo();
+        providerInfo.authority = CellBroadcastProvider.AUTHORITY;
+
+        // Add context to given carrierIdProvider
+        attachInfoForTesting(context, providerInfo);
+    }
+}
diff --git a/tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java b/tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java
new file mode 100644
index 0000000..9246f35
--- /dev/null
+++ b/tests/src/com/android/cellbroadcastservice/CellBroadcastServiceTestBase.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 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 com.android.cellbroadcastservice;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.os.IPowerManager;
+import android.os.PowerManager;
+import android.telephony.SubscriptionManager;
+import android.test.mock.MockContentResolver;
+import android.testing.TestableLooper;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * This is the test base class can be extended by all cell broadcast service unit test classes.
+ */
+public class CellBroadcastServiceTestBase extends TestCase {
+
+    @Mock
+    Context mMockedContext;
+
+    @Mock
+    Resources mMockedResources;
+
+    @Mock
+    SubscriptionManager mMockedSubscriptionManager;
+
+    final MockContentResolver mMockedContentResolver = new MockContentResolver();
+
+    private final HashMap<InstanceKey, Object> mOldInstances = new HashMap<>();
+
+    private final LinkedList<InstanceKey> mInstanceKeys = new LinkedList<>();
+
+    private static class InstanceKey {
+        final Class mClass;
+        final String mInstName;
+        final Object mObj;
+
+        InstanceKey(final Class c, final String instName, final Object obj) {
+            mClass = c;
+            mInstName = instName;
+            mObj = obj;
+        }
+
+        @Override
+        public int hashCode() {
+            return (mClass.getName().hashCode() * 31 + mInstName.hashCode()) * 31;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null || obj.getClass() != getClass()) {
+                return false;
+            }
+
+            InstanceKey other = (InstanceKey) obj;
+            return (other.mClass == mClass && other.mInstName.equals(mInstName)
+                    && other.mObj == mObj);
+        }
+    }
+
+    protected void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mMockedContentResolver).when(mMockedContext).getContentResolver();
+        doReturn(mMockedResources).when(mMockedContext).getResources();
+
+        // Can't directly mock power manager because it's final.
+        PowerManager powerManager = new PowerManager(mMockedContext, mock(IPowerManager.class),
+                new Handler(TestableLooper.get(CellBroadcastServiceTestBase.this).getLooper()));
+        doReturn(powerManager).when(mMockedContext).getSystemService(Context.POWER_SERVICE);
+        doReturn(mMockedSubscriptionManager).when(mMockedContext)
+                .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+        doReturn(new int[]{1}).when(mMockedSubscriptionManager).getSubscriptionIds(anyInt());
+    }
+
+    protected void tearDown() throws Exception {
+        restoreInstances();
+    }
+
+    void putResources(int id, Object value) {
+        if (value instanceof String[]) {
+            doReturn(value).when(mMockedResources).getStringArray(eq(id));
+        } else if (value instanceof Boolean) {
+            doReturn(value).when(mMockedResources).getBoolean(eq(id));
+        } else if (value instanceof Integer) {
+            doReturn(value).when(mMockedResources).getInteger(eq(id));
+        } else if (value instanceof Integer[]) {
+            doReturn(value).when(mMockedResources).getIntArray(eq(id));
+        } else if (value instanceof String) {
+            doReturn(value).when(mMockedResources).getString(eq(id));
+        }
+    }
+
+    synchronized void replaceInstance(final Class c, final String instanceName,
+                                              final Object obj, final Object newValue)
+            throws Exception {
+        Field field = c.getDeclaredField(instanceName);
+        field.setAccessible(true);
+
+        InstanceKey key = new InstanceKey(c, instanceName, obj);
+        if (!mOldInstances.containsKey(key)) {
+            mOldInstances.put(key, field.get(obj));
+            mInstanceKeys.add(key);
+        }
+        field.set(obj, newValue);
+    }
+
+    private synchronized void restoreInstances() throws Exception {
+        Iterator<InstanceKey> it = mInstanceKeys.descendingIterator();
+
+        while (it.hasNext()) {
+            InstanceKey key = it.next();
+            Field field = key.mClass.getDeclaredField(key.mInstName);
+            field.setAccessible(true);
+            field.set(key.mObj, mOldInstances.get(key));
+        }
+
+        mInstanceKeys.clear();
+        mOldInstances.clear();
+    }
+}