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();
+ }
+}