diff options
| author | android-build-team Robot <android-build-team-robot@google.com> | 2019-11-11 21:18:31 +0000 |
|---|---|---|
| committer | android-build-team Robot <android-build-team-robot@google.com> | 2019-11-11 21:18:31 +0000 |
| commit | 2b8514135718c9314baa7a66a13972fb4e82619d (patch) | |
| tree | eeabb840b9abe143cb6aae3ef519f1609d254c51 | |
| parent | ebf384ebdc42932a924d5f42e5a4fc281cee436a (diff) | |
| parent | e6a30d8437bb9110f75b3ddb196a54f800b7d98c (diff) | |
| download | platform_packages_apps_CellBroadcastReceiver-android10-mainline-resolv-release.tar.gz platform_packages_apps_CellBroadcastReceiver-android10-mainline-resolv-release.tar.bz2 platform_packages_apps_CellBroadcastReceiver-android10-mainline-resolv-release.zip | |
Snap for 6001391 from e6a30d8437bb9110f75b3ddb196a54f800b7d98c to qt-aml-resolv-releaseandroid10-mainline-resolv-release
Change-Id: Iae88e302d3970068f7194fb881fc7083bbf63851
60 files changed, 1112 insertions, 5483 deletions
diff --git a/Android.bp b/Android.bp index e6157e290..cff6ab8b1 100644 --- a/Android.bp +++ b/Android.bp @@ -4,10 +4,8 @@ java_defaults { min_sdk_version: "29", platform_apis: true, privileged: true, - // TODO remove framework-cellbroadcast-shared-srcs once move service to different repo srcs: [ "src/**/*.java", - ":framework-cellbroadcast-shared-srcs", ], libs: ["telephony-common"], static_libs: [ @@ -39,4 +37,4 @@ android_app { // CellBroadcastAppPlatform is a replacement for CellBroadcastApp overrides: ["CellBroadcastApp"], manifest: "AndroidManifest_Platform.xml", -}
\ No newline at end of file +} diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8fdbbba26..42b72beb1 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -18,7 +18,6 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.cellbroadcastreceiver" - android:sharedUserId="android.uid.phone" android:versionCode="300000000" android:versionName="R-initial"> @@ -34,7 +33,6 @@ <uses-permission android:name="android.permission.MANAGE_USERS" /> <uses-permission android:name="android.permission.DEVICE_POWER" /> <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" /> - <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-sdk android:minSdkVersion="21"/> @@ -49,15 +47,6 @@ <meta-data android:name="com.google.android.backup.api_key" android:value="AEdPqrEAAAAI2_Lb4sDI0e0twL-kf6GIqXpZIfrR0OhnM1pNJQ" /> - <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> - <service android:name="CellBroadcastAlertAudio" android:exported="false" /> @@ -70,8 +59,10 @@ <service android:name="CellBroadcastAlertReminder" android:exported="false" /> + <!-- Export provider for AT&T Device and Network Reset--> <provider android:name="CellBroadcastContentProvider" android:authorities="cellbroadcasts" + android:exported="true" android:readPermission="android.permission.READ_CELL_BROADCASTS" /> <activity android:name="CellBroadcastListActivity" diff --git a/AndroidManifest_Platform.xml b/AndroidManifest_Platform.xml index 76beac697..7cdd67d72 100644 --- a/AndroidManifest_Platform.xml +++ b/AndroidManifest_Platform.xml @@ -17,7 +17,6 @@ */ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - android:sharedUserId="android.uid.phone" package="com.android.cellbroadcastreceiver"> <original-package android:name="com.android.cellbroadcastreceiver" /> @@ -32,7 +31,6 @@ <uses-permission android:name="android.permission.MANAGE_USERS" /> <uses-permission android:name="android.permission.DEVICE_POWER" /> <uses-permission android:name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" /> - <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-sdk android:minSdkVersion="21"/> @@ -47,15 +45,6 @@ <meta-data android:name="com.google.android.backup.api_key" android:value="AEdPqrEAAAAI2_Lb4sDI0e0twL-kf6GIqXpZIfrR0OhnM1pNJQ" /> - <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> - <service android:name="CellBroadcastAlertAudio" android:exported="false" /> diff --git a/res/values-mcc313-mnc100/config.xml b/res/values-mcc226/config.xml index 423eaea57..4dee02eb0 100644 --- a/res/values-mcc313-mnc100/config.xml +++ b/res/values-mcc226/config.xml @@ -13,12 +13,8 @@ See the License for the specific language governing permissions and limitations under the License. --> -<resources> - <!-- Whether to compare message body when performing message duplicate dection --> - <bool name="duplicate_compare_body">true</bool> - <!-- Whether to reset alert message duplicate detection after toggling airplane mode --> - <bool name="reset_duplicate_detection_on_airplane_mode">true</bool> - <!-- Show timestamp of the alert --> - <bool name="show_date_time_title">true</bool> -</resources> +<resources> + <!-- Whether to display presidential alert in the settings --> + <bool name="show_presidential_alerts_in_settings">true</bool> +</resources>
\ No newline at end of file diff --git a/res/values-mcc312-mnc670 b/res/values-mcc312-mnc670 index 2d21ee5ba..78e65039e 120000 --- a/res/values-mcc312-mnc670 +++ b/res/values-mcc312-mnc670 @@ -1 +1 @@ -values-mcc313-mnc100
\ No newline at end of file +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 000000000..78e65039e --- /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 index 2d21ee5ba..78e65039e 120000 --- a/res/values-mcc313-mnc110 +++ b/res/values-mcc313-mnc110 @@ -1 +1 @@ -values-mcc313-mnc100
\ No newline at end of file +values-mcc310-mnc410
\ No newline at end of file diff --git a/res/values-mcc313-mnc120 b/res/values-mcc313-mnc120 index 2d21ee5ba..78e65039e 120000 --- a/res/values-mcc313-mnc120 +++ b/res/values-mcc313-mnc120 @@ -1 +1 @@ -values-mcc313-mnc100
\ No newline at end of file +values-mcc310-mnc410
\ No newline at end of file diff --git a/res/values-mcc313-mnc130 b/res/values-mcc313-mnc130 index 2d21ee5ba..78e65039e 120000 --- a/res/values-mcc313-mnc130 +++ b/res/values-mcc313-mnc130 @@ -1 +1 @@ -values-mcc313-mnc100
\ No newline at end of file +values-mcc310-mnc410
\ No newline at end of file diff --git a/res/values-mcc313-mnc140 b/res/values-mcc313-mnc140 index 2d21ee5ba..78e65039e 120000 --- a/res/values-mcc313-mnc140 +++ b/res/values-mcc313-mnc140 @@ -1 +1 @@ -values-mcc313-mnc100
\ No newline at end of file +values-mcc310-mnc410
\ No newline at end of file diff --git a/res/values-mcc314 b/res/values-mcc314 new file mode 120000 index 000000000..e454f498c --- /dev/null +++ b/res/values-mcc314 @@ -0,0 +1 @@ +values-mcc310
\ No newline at end of file diff --git a/res/values-mcc314/config.xml b/res/values-mcc314/config.xml deleted file mode 120000 index 0dc8a9de1..000000000 --- a/res/values-mcc314/config.xml +++ /dev/null @@ -1 +0,0 @@ -../values-mcc310/config.xml
\ No newline at end of file diff --git a/res/values-mcc315 b/res/values-mcc315 new file mode 120000 index 000000000..e454f498c --- /dev/null +++ b/res/values-mcc315 @@ -0,0 +1 @@ +values-mcc310
\ No newline at end of file diff --git a/res/values-mcc315/config.xml b/res/values-mcc315/config.xml deleted file mode 120000 index 0dc8a9de1..000000000 --- a/res/values-mcc315/config.xml +++ /dev/null @@ -1 +0,0 @@ -../values-mcc310/config.xml
\ No newline at end of file diff --git a/res/values-mcc316 b/res/values-mcc316 new file mode 120000 index 000000000..e454f498c --- /dev/null +++ b/res/values-mcc316 @@ -0,0 +1 @@ +values-mcc310
\ No newline at end of file diff --git a/res/values-mcc316/config.xml b/res/values-mcc316/config.xml deleted file mode 120000 index 0dc8a9de1..000000000 --- a/res/values-mcc316/config.xml +++ /dev/null @@ -1 +0,0 @@ -../values-mcc310/config.xml
\ No newline at end of file diff --git a/res/values-mcc424/strings.xml b/res/values-mcc424/strings.xml index 3d5065938..78e27b456 100644 --- a/res/values-mcc424/strings.xml +++ b/res/values-mcc424/strings.xml @@ -24,4 +24,5 @@ <string name="cmas_severe_alert" translatable="false">@string/emergency_alert</string> <string name="cmas_amber_alert">Warning alert</string> <string name="cmas_required_monthly_test">Test alert</string> -</resources>
\ No newline at end of file + <string name="cmas_exercise_alert">Exercise</string> +</resources> diff --git a/res/values-mcc530/config.xml b/res/values-mcc530/config.xml index 38c5bd810..dce586462 100644 --- a/res/values-mcc530/config.xml +++ b/res/values-mcc530/config.xml @@ -17,4 +17,8 @@ <resources> <bool name="show_date_time_title">true</bool> <bool name="use_full_volume">true</bool> + <!-- always bypass user settings and play presidential alert at full volumes --> + <bool name="full_volume_presidential_alert">true</bool> + <!-- Disable Amber alert--> + <string-array name="cmas_amber_alerts_channels_range_strings" translatable="false"></string-array> </resources> diff --git a/res/values-mcc530/strings.xml b/res/values-mcc530/strings.xml index 1fcf240ef..a2a292fbe 100644 --- a/res/values-mcc530/strings.xml +++ b/res/values-mcc530/strings.xml @@ -18,4 +18,12 @@ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <!-- Special New Zealand requirement for the presidential alert title --> <string name="cmas_presidential_level_alert" translatable="false">@string/emergency_alert</string> + <!-- CMAS dialog title for extreme alert. [CHAR LIMIT=50] --> + <string name="cmas_extreme_alert">@string/emergency_alert</string> + <!-- CMAS dialog title for extreme alert with extreme severity, immediate urgency, and observed certainty. [CHAR LIMIT=50] --> + <string name="cmas_extreme_immediate_observed_alert">@string/emergency_alert</string> + <!-- CMAS dialog title for extreme alert with extreme severity, immediate urgency, and likely certainty. [CHAR LIMIT=50] --> + <string name="cmas_extreme_immediate_likely_alert">@string/emergency_alert</string> + <!-- CMAS dialog title for severe alert. [CHAR LIMIT=50] --> + <string name="cmas_severe_alert">@string/emergency_alert</string> </resources> diff --git a/res/values-mcc716/config.xml b/res/values-mcc716/config.xml index ef47e57f2..5752117d3 100644 --- a/res/values-mcc716/config.xml +++ b/res/values-mcc716/config.xml @@ -63,4 +63,7 @@ <!-- Disable links for Peru --> <integer-array name="message_classes_to_linkify"></integer-array> + + <!-- Hide public safety settings per Peru users --> + <bool name="show_public_safety_settings">false</bool> </resources> diff --git a/res/values/config.xml b/res/values/config.xml index 2b1f4e1b9..a160b7812 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -15,7 +15,7 @@ --> <resources> - <!-- Whether to always sound CBS alerts at full volume --> + <!-- Whether to bypass Do-Not-Disturb/silent mode and play emergency alert tone at full volume. --> <bool name="use_full_volume">false</bool> <!-- Whether to enable CMAS settings (United States) --> <bool name="show_cmas_settings">true</bool> @@ -33,7 +33,12 @@ <bool name="show_cmas_messages_in_priority_order">false</bool> <!-- Whether to ignore any alert message in ECBM(Emergency Callback Mode) --> <bool name="ignore_messages_in_ecbm">false</bool> - + <!-- Show checkbox for Presidential alerts in settings --> + <bool name="show_presidential_alerts_in_settings">false</bool> + <!-- Whether to display public safety alert settings, some countries/carriers want to enable it by default and not allow users to disable --> + <bool name="show_public_safety_settings">true</bool> + <!-- Whether to bypass Do-Not-Disturb/silent mode and play emergency alert tone at full volume for presidential alert --> + <bool name="full_volume_presidential_alert">false</bool> <!-- 4370, 4383 --> <string-array name="cmas_presidential_alerts_channels_range_strings" translatable="false"> <item>0x1112:rat=gsm, emergency=true</item> diff --git a/res/values/strings.xml b/res/values/strings.xml index 55379cf87..feae57f34 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -278,4 +278,10 @@ <!-- Label to open emergencyNotifications dialog [CHAR LIMIT=40] --> <string name="emergency_alert_settings_title_watches">Wireless emergency alerts</string> + + <!-- Show checkbox for Presidential alerts in settings --> + <!-- Preference title for enable presidential threat alerts checkbox. [CHAR LIMIT=40] --> + <string name="enable_cmas_presidential_alerts_title">Presidential alerts</string> + <!-- Preference summary for enable presidential threat alerts checkbox. [CHAR LIMIT=100] --> + <string name="enable_cmas_presidential_alerts_summary">National warning messages issued by the President. Can\'t be turned off.</string> </resources> diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index e72f44625..8b9698da2 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -33,6 +33,13 @@ android:summary="@string/enable_emergency_alerts_message_summary" android:title="@string/enable_emergency_alerts_message_title" /> + <!-- Show checkbox for Presidential alerts in settings --> + <SwitchPreference android:defaultValue="true" + android:enabled="false" + android:key="enable_cmas_presidential_alerts" + android:summary="@string/enable_cmas_presidential_alerts_summary" + android:title="@string/enable_cmas_presidential_alerts_title"/> + <!-- Enable CMAS AMBER alerts --> <SwitchPreference android:defaultValue="true" android:key="enable_cmas_amber_alerts" diff --git a/src/com/android/cellbroadcastreceiver/CbGeoUtils.java b/src/com/android/cellbroadcastreceiver/CbGeoUtils.java deleted file mode 100644 index 9a8697ad4..000000000 --- a/src/com/android/cellbroadcastreceiver/CbGeoUtils.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * 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.cellbroadcastreceiver; - -import android.annotation.NonNull; -import android.telephony.Rlog; -import android.text.TextUtils; - -import com.android.internal.telephony.CbGeoUtils.Geometry; -import com.android.internal.telephony.CbGeoUtils.LatLng; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * This utils class is specifically used for geo-targeting of CellBroadcast messages. - * The coordinates used by this utils class are latitude and longitude, but some algorithms in this - * class only use them as coordinates on plane, so the calculation will be inaccurate. So don't use - * this class for anything other then geo-targeting of cellbroadcast messages. - */ -public class CbGeoUtils { - /** - * Tolerance for determining if the value is 0. If the absolute value of a value is less than - * this tolerance, it will be treated as 0. - */ - public static final double EPS = 1e-7; - - private static final String TAG = "CbGeoUtils"; - - /** The TLV tags of WAC, defined in ATIS-0700041 5.2.3 WAC tag coding. */ - public static final int GEO_FENCING_MAXIMUM_WAIT_TIME = 0x01; - public static final int GEOMETRY_TYPE_POLYGON = 0x02; - public static final int GEOMETRY_TYPE_CIRCLE = 0x03; - - /** The identifier of geometry in the encoded string. */ - private static final String CIRCLE_SYMBOL = "circle"; - private static final String POLYGON_SYMBOL = "polygon"; - - /** - * The class represents a simple polygon with at least 3 points. - */ - public static class Polygon implements com.android.internal.telephony.CbGeoUtils.Geometry { - /** - * In order to reduce the loss of precision in floating point calculations, all vertices - * of the polygon are scaled. Set the value of scale to 1000 can take into account the - * actual distance accuracy of 1 meter if the EPS is 1e-7 during the calculation. - */ - private static final double SCALE = 1000.0; - - private final List<LatLng> mVertices; - private final List<Point> mScaledVertices; - private final LatLng mOrigin; - - /** - * Constructs a simple polygon from the given vertices. The adjacent two vertices are - * connected to form an edge of the polygon. The polygon has at least 3 vertices, and the - * last vertices and the first vertices must be adjacent. - * - * The longitude difference in the vertices should be less than 180 degree. - */ - public Polygon(@NonNull List<LatLng> vertices) { - mVertices = vertices; - - // Find the point with smallest longitude as the mOrigin point. - int idx = 0; - for (int i = 1; i < vertices.size(); i++) { - if (vertices.get(i).lng < vertices.get(idx).lng) { - idx = i; - } - } - mOrigin = vertices.get(idx); - - mScaledVertices = vertices.stream() - .map(latLng -> convertAndScaleLatLng(latLng)) - .collect(Collectors.toList()); - } - - public List<LatLng> getVertices() { - return mVertices; - } - - /** - * Check if the given point {@code p} is inside the polygon. This method counts the number - * of times the polygon winds around the point P, A.K.A "winding number". The point is - * outside only when this "winding number" is 0. - * - * If a point is on the edge of the polygon, it is also considered to be inside the polygon. - */ - @Override - public boolean contains(LatLng latLng) { - Point p = convertAndScaleLatLng(latLng); - - int n = mScaledVertices.size(); - int windingNumber = 0; - for (int i = 0; i < n; i++) { - Point a = mScaledVertices.get(i); - Point b = mScaledVertices.get((i + 1) % n); - - // CCW is counterclockwise - // CCW = ab x ap - // CCW > 0 -> ap is on the left side of ab - // CCW == 0 -> ap is on the same line of ab - // CCW < 0 -> ap is on the right side of ab - int ccw = sign(crossProduct(b.subtract(a), p.subtract(a))); - - if (ccw == 0) { - if (Math.min(a.x, b.x) <= p.x && p.x <= Math.max(a.x, b.x) - && Math.min(a.y, b.y) <= p.y && p.y <= Math.max(a.y, b.y)) { - return true; - } - } else { - if (sign(a.y - p.y) <= 0) { - // upward crossing - if (ccw > 0 && sign(b.y - p.y) > 0) { - ++windingNumber; - } - } else { - // downward crossing - if (ccw < 0 && sign(b.y - p.y) <= 0) { - --windingNumber; - } - } - } - } - return windingNumber != 0; - } - - /** - * Move the given point {@code latLng} to the coordinate system with {@code mOrigin} as the - * origin and scale it. {@code mOrigin} is selected from the vertices of a polygon, it has - * the smallest longitude value among all of the polygon vertices. - * - * @param latLng the point need to be converted and scaled. - * @Return a {@link Point} object. - */ - private Point convertAndScaleLatLng(LatLng latLng) { - double x = latLng.lat - mOrigin.lat; - double y = latLng.lng - mOrigin.lng; - - // If the point is in different hemispheres(western/eastern) than the mOrigin, and the - // edge between them cross the 180th meridian, then its relative coordinates will be - // extended. - // For example, suppose the longitude of the mOrigin is -178, and the longitude of the - // point to be converted is 175, then the longitude after the conversion is -8. - // calculation: (-178 - 8) - (-178). - if (sign(mOrigin.lng) != 0 && sign(mOrigin.lng) != sign(latLng.lng)) { - double distCross0thMeridian = Math.abs(mOrigin.lng) + Math.abs(latLng.lng); - if (sign(distCross0thMeridian * 2 - 360) > 0) { - y = sign(mOrigin.lng) * (360 - distCross0thMeridian); - } - } - return new Point(x * SCALE, y * SCALE); - } - - private static double crossProduct(Point a, Point b) { - return a.x * b.y - a.y * b.x; - } - - static final class Point { - public final double x; - public final double y; - - Point(double x, double y) { - this.x = x; - this.y = y; - } - - public Point subtract(Point p) { - return new Point(x - p.x, y - p.y); - } - } - } - - /** The class represents a circle. */ - public static class Circle implements Geometry { - private final LatLng mCenter; - private final double mRadiusMeter; - - public Circle(LatLng center, double radiusMeter) { - this.mCenter = center; - this.mRadiusMeter = radiusMeter; - } - - public LatLng getCenter() { - return mCenter; - } - - public double getRadius() { - return mRadiusMeter; - } - - @Override - public boolean contains(LatLng p) { - return mCenter.distance(p) <= mRadiusMeter; - } - } - - /** - * Parse the geometries from the encoded string {@code str}. The string must follow the - * geometry encoding specified by {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}. - */ - @NonNull - public static List<Geometry> parseGeometriesFromString(@NonNull String str) { - List<Geometry> geometries = new ArrayList<>(); - for (String geometryStr : str.split("\\s*;\\s*")) { - String[] geoParameters = geometryStr.split("\\s*\\|\\s*"); - switch (geoParameters[0]) { - case CIRCLE_SYMBOL: - geometries.add(new Circle(parseLatLngFromString(geoParameters[1]), - Double.parseDouble(geoParameters[2]))); - break; - case POLYGON_SYMBOL: - List<LatLng> vertices = new ArrayList<>(geoParameters.length - 1); - for (int i = 1; i < geoParameters.length; i++) { - vertices.add(parseLatLngFromString(geoParameters[i])); - } - geometries.add(new Polygon(vertices)); - break; - default: - Rlog.e(TAG, "Invalid geometry format " + geometryStr); - } - } - return geometries; - } - - /** - * Encode a list of geometry objects to string. The encoding format is specified by - * {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}. - * - * @param geometries the list of geometry objects need to be encoded. - * @return the encoded string. - */ - @NonNull - public static String encodeGeometriesToString(@NonNull List<Geometry> geometries) { - return geometries.stream() - .map(geometry -> encodeGeometryToString(geometry)) - .filter(encodedStr -> !TextUtils.isEmpty(encodedStr)) - .collect(Collectors.joining(";")); - } - - - /** - * Encode the geometry object to string. The encoding format is specified by - * {@link android.provider.Telephony.CellBroadcasts#GEOMETRIES}. - * @param geometry the geometry object need to be encoded. - * @return the encoded string. - */ - @NonNull - private static String encodeGeometryToString(@NonNull Geometry geometry) { - StringBuilder sb = new StringBuilder(); - if (geometry instanceof Polygon) { - sb.append(POLYGON_SYMBOL); - for (LatLng latLng : ((Polygon) geometry).getVertices()) { - sb.append("|"); - sb.append(latLng.lat); - sb.append(","); - sb.append(latLng.lng); - } - } else if (geometry instanceof Circle) { - sb.append(CIRCLE_SYMBOL); - Circle circle = (Circle) geometry; - - // Center - sb.append("|"); - sb.append(circle.getCenter().lat); - sb.append(","); - sb.append(circle.getCenter().lng); - - // Radius - sb.append("|"); - sb.append(circle.getRadius()); - } else { - Rlog.e(TAG, "Unsupported geometry object " + geometry); - return null; - } - return sb.toString(); - } - - /** - * Parse {@link LatLng} from {@link String}. Latitude and longitude are separated by ",". - * Example: "13.56,-55.447". - * - * @param str encoded lat/lng string. - * @Return {@link LatLng} object. - */ - @NonNull - public static LatLng parseLatLngFromString(@NonNull String str) { - String[] latLng = str.split("\\s*,\\s*"); - return new LatLng(Double.parseDouble(latLng[0]), Double.parseDouble(latLng[1])); - } - - /** - * @Return the sign of the given value {@code value} with the specified tolerance. Return 1 - * means the sign is positive, -1 means negative, 0 means the value will be treated as 0. - */ - public static int sign(double value) { - if (value > EPS) return 1; - if (value < -EPS) return -1; - return 0; - } -} diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java index 05d720748..78ffb20ea 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java @@ -16,6 +16,8 @@ package com.android.cellbroadcastreceiver; +import static android.telephony.PhoneStateListener.LISTEN_NONE; + import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; import android.app.Service; @@ -39,12 +41,10 @@ import android.preference.PreferenceManager; import android.provider.Settings; import android.speech.tts.TextToSpeech; import android.telephony.PhoneStateListener; -import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; -import android.util.SparseArray; import com.android.cellbroadcastreceiver.CellBroadcastAlertService.AlertType; @@ -75,7 +75,15 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI /** Extra for alert vibration pattern (unless master volume is silent). */ public static final String ALERT_AUDIO_VIBRATION_PATTERN_EXTRA = - "com.android.cellbroadcastreceiver.ALERT_VIBRATION_PATTERN"; + "com.android.cellbroadcastreceiver.ALERT_AUDIO_VIBRATION_PATTERN"; + + /** Extra for always sound alerts at full volume. */ + public static final String ALERT_AUDIO_FULL_VOLUME_EXTRA = + "com.android.cellbroadcastreceiver.ALERT_FULL_VOLUME_EXTRA"; + + /** Extra for alert subscription index */ + public static final String ALERT_AUDIO_SUB_INDEX = + "com.android.cellbroadcastreceiver.ALERT_AUDIO_SUB_INDEX"; private static final String TTS_UTTERANCE_ID = "com.android.cellbroadcastreceiver.UTTERANCE_ID"; @@ -92,8 +100,10 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI private TextToSpeech mTts; private boolean mTtsEngineReady; + private AlertType mAlertType; private String mMessageBody; private String mMessageLanguage; + private int mSubId; private boolean mTtsLanguageSupported; private boolean mEnableVibrate; private boolean mEnableAudio; @@ -106,9 +116,6 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI private MediaPlayer mMediaPlayer; private AudioManager mAudioManager; private TelephonyManager mTelephonyManager; - private SubscriptionManager mSubscriptionManager; - private int mPhoneCount; - private SparseArray<PhoneStateListener> mPhoneStateListeners = new SparseArray<>(); private int mInitialCallState; // Internal messages @@ -143,7 +150,7 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI if (mMessageBody != null && mTtsEngineReady && mTtsLanguageSupported) { if (DBG) log("Speaking broadcast text: " + mMessageBody); - mTts.setAudioAttributes(getAlertAudioAttributes()); + mTts.setAudioAttributes(getAlertAudioAttributes(mAlertType)); res = mTts.speak(mMessageBody, 2, null, TTS_UTTERANCE_ID); mState = STATE_SPEAKING; } @@ -156,6 +163,17 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI default: loge("Handler received unknown message, what=" + msg.what); + } + } + }; + + private final PhoneStateListener mPhoneStateListener = new PhoneStateListener() { + @Override + public void onCallStateChanged(int state, String ignored) { + // Stop the alert sound and speech if the call state changes. + if (state != TelephonyManager.CALL_STATE_IDLE + && state != mInitialCallState) { + stopSelf(); } } }; @@ -220,34 +238,8 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); // Listen for incoming calls to kill the alarm. - mTelephonyManager = - (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); - mSubscriptionManager = - (SubscriptionManager) getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE); - - mPhoneCount = mTelephonyManager.getMaxPhoneCount(); - for (int i = 0; i < mPhoneCount; i++) { - final SubscriptionInfo sir = - mSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(i); - if (sir == null) continue; - final int subId = sir.getSubscriptionId(); - PhoneStateListener listener = - new PhoneStateListener() { - @Override - public void onCallStateChanged(int state, String ignored) { - // Stop the alert sound and speech if the call state changes. - if (state != TelephonyManager.CALL_STATE_IDLE - && state != mInitialCallState) { - stopSelf(); - } - } - }; - - mPhoneStateListeners.put(subId, listener); - mTelephonyManager - .createForSubscriptionId(subId) - .listen(listener, PhoneStateListener.LISTEN_CALL_STATE); - } + mTelephonyManager = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)); + mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); } @Override @@ -255,17 +247,7 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI // stop audio, vibration and TTS stop(); // Stop listening for incoming calls. - for (int i = 0; i < mPhoneCount; i++) { - final SubscriptionInfo sir = - mSubscriptionManager.getActiveSubscriptionInfoForSimSlotIndex(i); - if (sir == null) continue; - final int subId = sir.getSubscriptionId(); - PhoneStateListener listener = mPhoneStateListeners.get(subId); - if (listener == null) continue; - mTelephonyManager - .createForSubscriptionId(subId) - .listen(listener, PhoneStateListener.LISTEN_NONE); - } + mTelephonyManager.listen(mPhoneStateListener, LISTEN_NONE); // shutdown TTS engine if (mTts != null) { try { @@ -299,16 +281,22 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI // Get text to speak (if enabled by user) mMessageBody = intent.getStringExtra(ALERT_AUDIO_MESSAGE_BODY); mMessageLanguage = intent.getStringExtra(ALERT_AUDIO_MESSAGE_LANGUAGE); + mSubId = intent.getIntExtra(ALERT_AUDIO_SUB_INDEX, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - // Get config of whether to always sound CBS alerts at full volume. - mUseFullVolume = prefs.getBoolean(CellBroadcastSettings.KEY_USE_FULL_VOLUME, false); - + // retrieve whether always sound CBS alerts at full volume. + mUseFullVolume = intent.getBooleanExtra(ALERT_AUDIO_FULL_VOLUME_EXTRA, false); // retrieve the vibrate settings from cellbroadcast receiver settings. mEnableVibrate = prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_VIBRATE, true); - // retrieve the vibration patterns + // retrieve the vibration patterns. mVibrationPattern = intent.getIntArrayExtra(ALERT_AUDIO_VIBRATION_PATTERN_EXTRA); + // retrieve the alert type + mAlertType = AlertType.DEFAULT; + if (intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE) != null) { + mAlertType = (AlertType) intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE); + } switch (mAudioManager.getRingerMode()) { case AudioManager.RINGER_MODE_SILENT: @@ -342,11 +330,7 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI } if (mEnableAudio || mEnableVibrate) { - AlertType alertType = AlertType.DEFAULT; - if (intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE) != null) { - alertType = (AlertType) intent.getSerializableExtra(ALERT_AUDIO_TONE_TYPE); - } - playAlertTone(alertType, mVibrationPattern); + playAlertTone(mAlertType, mVibrationPattern); } else { stopSelf(); return START_NOT_STICKY; @@ -373,10 +357,9 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI stop(); log("playAlertTone: alertType=" + alertType + ", mEnableVibrate=" + mEnableVibrate - + ", mEnableAudio=" + mEnableAudio + ", mUseFullVolume=" + mUseFullVolume); - Resources res = - CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId( - getApplicationContext()); + + ", mEnableAudio=" + mEnableAudio + ", mUseFullVolume=" + mUseFullVolume + + ", mSubId=" + mSubId); + Resources res = CellBroadcastSettings.getResources(getApplicationContext(), mSubId); // Vibration duration in milliseconds long vibrateDuration = 0; @@ -395,7 +378,8 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI } AudioAttributes.Builder attrBuilder = new AudioAttributes.Builder(); - attrBuilder.setUsage(AudioAttributes.USAGE_ALARM); + attrBuilder.setUsage(alertType == AlertType.INFO + ? AudioAttributes.USAGE_NOTIFICATION : AudioAttributes.USAGE_ALARM); if (mUseFullVolume) { // Set the flags to bypass DnD mode if the user enables use full volume option. attrBuilder.setFlags(AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY @@ -471,8 +455,8 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI // Request audio focus (though we're going to play even if we don't get it) mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); - mMediaPlayer.setAudioAttributes(getAlertAudioAttributes()); - setAlertVolume(); + mMediaPlayer.setAudioAttributes(getAlertAudioAttributes(mAlertType)); + setAlertVolume(mAlertType); // If we are using the custom alert duration, set looping to true so we can repeat // the alert. The tone playing will stop when ALERT_SOUND_FINISHED arrives. @@ -518,7 +502,7 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI mHandler.removeMessages(ALERT_SOUND_FINISHED); mHandler.removeMessages(ALERT_PAUSE_FINISHED); - resetAlarmStreamVolume(); + resetAlarmStreamVolume(mAlertType); if (mState == STATE_ALERTING) { // Stop audio playing @@ -549,11 +533,12 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI /** * Get audio attribute for the alarm. */ - private AudioAttributes getAlertAudioAttributes() { + private AudioAttributes getAlertAudioAttributes(AlertType alertType) { AudioAttributes.Builder builder = new AudioAttributes.Builder(); builder.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION); - builder.setUsage(AudioAttributes.USAGE_ALARM); + builder.setUsage((alertType == AlertType.INFO + ? AudioAttributes.USAGE_NOTIFICATION : AudioAttributes.USAGE_ALARM)); if (mUseFullVolume) { // Set FLAG_BYPASS_INTERRUPTION_POLICY and FLAG_BYPASS_MUTE so that it enables // audio in any DnD mode, even in total silence DnD mode (requires MODIFY_PHONE_STATE). @@ -567,7 +552,7 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI /** * Set volume for alerts. */ - private void setAlertVolume() { + private void setAlertVolume(AlertType alertType) { if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE || isOnEarphone()) { // If we are in a call, play the alert @@ -578,7 +563,7 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI // If use_full_volume is configured, // we overwrite volume setting of STREAM_ALARM to full, play at // max possible volume, and reset it after it's finished. - setAlarmStreamVolumeToFull(); + setAlarmStreamVolumeToFull(alertType); } } @@ -601,22 +586,25 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI /** * Set volume of STREAM_ALARM to full. */ - private void setAlarmStreamVolumeToFull() { + private void setAlarmStreamVolumeToFull(AlertType alertType) { log("setting alarm volume to full for cell broadcast alerts."); - mUserSetAlarmVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM); + int streamType = (alertType == AlertType.INFO) + ? AudioManager.STREAM_NOTIFICATION : AudioManager.STREAM_ALARM; + mUserSetAlarmVolume = mAudioManager.getStreamVolume(streamType); mResetAlarmVolumeNeeded = true; - mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, - mAudioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM), - 0); + mAudioManager.setStreamVolume(streamType, + mAudioManager.getStreamMaxVolume(streamType), 0); } /** * Reset volume of STREAM_ALARM, if needed. */ - private void resetAlarmStreamVolume() { + private void resetAlarmStreamVolume(AlertType alertType) { if (mResetAlarmVolumeNeeded) { log("resetting alarm volume to back to " + mUserSetAlarmVolume); - mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, mUserSetAlarmVolume, 0); + mAudioManager.setStreamVolume(alertType == AlertType.INFO + ? AudioManager.STREAM_NOTIFICATION : AudioManager.STREAM_ALARM, + mUserSetAlarmVolume, 0); mResetAlarmVolumeNeeded = false; } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java index 02c8831ae..c07416646 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java @@ -29,11 +29,10 @@ import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.PowerManager; -import android.os.UserManager; import android.preference.PreferenceManager; import android.provider.Telephony; -import android.telephony.CellBroadcastMessage; import android.telephony.SmsCbCmasInfo; +import android.telephony.SubscriptionManager; import android.text.util.Linkify; import android.util.Log; import android.view.KeyEvent; @@ -109,8 +108,8 @@ public class CellBroadcastAlertDialog extends Activity { AnimationHandler() {} /** Start the warning icon animation. */ - void startIconAnimation() { - if (!initDrawableAndImageView()) { + void startIconAnimation(int subId) { + if (!initDrawableAndImageView(subId)) { return; // init failure } mWarningIconVisible = true; @@ -152,13 +151,16 @@ public class CellBroadcastAlertDialog extends Activity { /** * Initialize the Drawable and ImageView fields. + * + * @param subId Subscription index + * * @return true if successful; false if any field failed to initialize */ - private boolean initDrawableAndImageView() { + private boolean initDrawableAndImageView(int subId) { if (mWarningIcon == null) { try { - mWarningIcon = CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId( - getApplicationContext()).getDrawable(R.drawable.ic_warning_googred); + mWarningIcon = CellBroadcastSettings.getResources(getApplicationContext(), + subId).getDrawable(R.drawable.ic_warning_googred); } catch (Resources.NotFoundException e) { Log.e(TAG, "warning icon resource not found", e); return false; @@ -278,10 +280,14 @@ public class CellBroadcastAlertDialog extends Activity { // For emergency alerts, keep screen on so the user can read it CellBroadcastMessage message = getLatestMessage(); - if (message != null && CellBroadcastChannelManager.isEmergencyMessage( - this, message)) { - Log.d(TAG, "onCreate setting screen on timer for emergency alert"); - mScreenOffHandler.startScreenOnTimer(); + if (message != null) { + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + this, message.getSubId(this)); + if (channelManager.isEmergencyMessage(message)) { + Log.d(TAG, "onCreate setting screen on timer for emergency alert for sub " + + message.getSubId(this)); + mScreenOffHandler.startScreenOnTimer(); + } } updateAlertText(message); @@ -295,8 +301,13 @@ public class CellBroadcastAlertDialog extends Activity { protected void onResume() { super.onResume(); CellBroadcastMessage message = getLatestMessage(); - if (message != null && CellBroadcastChannelManager.isEmergencyMessage(this, message)) { - mAnimationHandler.startIconAnimation(); + if (message != null) { + int subId = message.getSubId(this); + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(this, + subId); + if (channelManager.isEmergencyMessage(message)) { + mAnimationHandler.startIconAnimation(subId); + } } } @@ -365,7 +376,7 @@ public class CellBroadcastAlertDialog extends Activity { String title = getText(titleId).toString(); TextView titleTextView = findViewById(R.id.alertTitle); - if (CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context) + if (CellBroadcastSettings.getResources(context, message.getSubId(context)) .getBoolean(R.bool.show_date_time_title)) { titleTextView.setSingleLine(false); title += "\n" + message.getDateString(context); @@ -425,8 +436,8 @@ public class CellBroadcastAlertDialog extends Activity { mMessageList = newMessageList; } else { mMessageList.addAll(newMessageList); - if (CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId( - getApplicationContext()) + if (CellBroadcastSettings.getResources(getApplicationContext(), + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) .getBoolean(R.bool.show_cmas_messages_in_priority_order)) { // Sort message list to show messages in a different order than received by // prioritizing them. Presidential Alert only has top priority. @@ -520,9 +531,11 @@ public class CellBroadcastAlertDialog extends Activity { CellBroadcastMessage nextMessage = getLatestMessage(); if (nextMessage != null) { updateAlertText(nextMessage); - if (CellBroadcastChannelManager.isEmergencyMessage( - this, nextMessage)) { - mAnimationHandler.startIconAnimation(); + int subId = nextMessage.getSubId(getApplicationContext()); + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + getApplicationContext(), subId); + if (channelManager.isEmergencyMessage(nextMessage)) { + mAnimationHandler.startIconAnimation(subId); } else { mAnimationHandler.stopIconAnimation(); } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java index 99bdeadec..e77d3e66b 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java @@ -39,7 +39,7 @@ import android.os.UserHandle; import android.preference.PreferenceManager; import android.provider.Telephony; import android.telephony.CarrierConfigManager; -import android.telephony.CellBroadcastMessage; +import android.telephony.SmsCbCmasInfo; import android.telephony.SmsCbEtwsInfo; import android.telephony.SmsCbLocation; import android.telephony.SmsCbMessage; @@ -104,6 +104,8 @@ public class CellBroadcastAlertService extends Service { private static final String MESSAGE_FILTER_PROPERTY_KEY = "persist.cellbroadcast.message_filter"; + private Context mContext; + /** * Alert type */ @@ -180,6 +182,7 @@ public class CellBroadcastAlertService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { + mContext = getApplicationContext(); String action = intent.getAction(); Log.d(TAG, "onStartCommand: " + action); if (Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION.equals(action) || @@ -215,8 +218,8 @@ public class CellBroadcastAlertService extends Service { * values indicate the duplicate will always be ignored. The default value would be 24 hours. */ private long getDuplicateExpirationTime(int subId) { - CarrierConfigManager configManager = (CarrierConfigManager) - getApplicationContext().getSystemService(Context.CARRIER_CONFIG_SERVICE); + CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService( + Context.CARRIER_CONFIG_SERVICE); Log.d(TAG, "manager = " + configManager); if (configManager == null) { Log.e(TAG, "carrier config is not available."); @@ -241,14 +244,10 @@ public class CellBroadcastAlertService extends Service { * @return True if the message should be displayed to the user */ private boolean shouldDisplayMessage(CellBroadcastMessage cbm) { - TelephonyManager tm = - ((TelephonyManager) - getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE)) - .createForSubscriptionId(cbm.getSubId()); - if (tm.getEmergencyCallbackMode() - && CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId( - getApplicationContext()) - .getBoolean(R.bool.ignore_messages_in_ecbm)) { + TelephonyManager tm = ((TelephonyManager) mContext.getSystemService( + Context.TELEPHONY_SERVICE)).createForSubscriptionId(cbm.getSubId(mContext)); + if (tm.getEmergencyCallbackMode() && CellBroadcastSettings.getResources( + mContext, cbm.getSubId(mContext)).getBoolean(R.bool.ignore_messages_in_ecbm)) { // Ignore the message in ECBM. // It is for LTE only mode. For 1xRTT, incoming pages should be ignored in the modem. Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() + " in ECBM"); @@ -269,8 +268,10 @@ public class CellBroadcastAlertService extends Service { } // Check if we need to perform language filtering. - CellBroadcastChannelRange range = CellBroadcastChannelManager - .getCellBroadcastChannelRangeFromMessage(getApplicationContext(), cbm); + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(mContext, + cbm.getSubId(mContext)); + CellBroadcastChannelRange range = channelManager + .getCellBroadcastChannelRangeFromMessage(cbm); if (range != null && range.mFilterLanguage) { // If the message's language does not match device's message, we don't display the // message. @@ -316,12 +317,7 @@ public class CellBroadcastAlertService extends Service { } final CellBroadcastMessage cbm = new CellBroadcastMessage(message); - int subId = intent.getExtras().getInt(PhoneConstants.SUBSCRIPTION_KEY); - if (SubscriptionManager.isValidSubscriptionId(subId)) { - cbm.setSubId(subId); - } else { - Log.e(TAG, "Invalid subscription id"); - } + int subId = cbm.getSubId(mContext); if (!shouldDisplayMessage(cbm)) { return; @@ -329,8 +325,7 @@ public class CellBroadcastAlertService extends Service { // Check if message body should be used for duplicate detection. boolean shouldCompareMessageBody = - CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId( - getApplicationContext()) + CellBroadcastSettings.getResources(mContext, cbm.getSubId(mContext)) .getBoolean(R.bool.duplicate_compare_body); int hashCode = shouldCompareMessageBody ? message.getMessageBody().hashCode() : 0; @@ -417,7 +412,9 @@ public class CellBroadcastAlertService extends Service { return; } - if (CellBroadcastChannelManager.isEmergencyMessage(this, cbm)) { + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + mContext, cbm.getSubId(mContext)); + if (channelManager.isEmergencyMessage(cbm)) { // start alert sound / vibration / TTS and display full-screen alert openEmergencyAlertNotification(cbm); } else { @@ -465,108 +462,109 @@ public class CellBroadcastAlertService extends Service { // Check if the messages are on additional channels enabled by the resource config. // If those channels are enabled by the carrier, but the device is actually roaming, we // should not allow the messages. - ArrayList<CellBroadcastChannelRange> ranges = - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - getApplicationContext(), R.array.additional_cbs_channels_strings); - - if (ranges != null) { - for (CellBroadcastChannelRange range : ranges) { - if (range.mStartId <= channel && range.mEndId >= channel) { - // Check if the channel is within the scope. If not, ignore the alert message. - if (!CellBroadcastChannelManager.checkScope(getApplicationContext(), - message.getSubId(), range.mScope)) { - Log.d(TAG, "The range [" + range.mStartId + "-" + range.mEndId - + "] is not within the scope. mScope = " + range.mScope); - return false; - } + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + mContext, message.getSubId(mContext)); + ArrayList<CellBroadcastChannelRange> ranges = channelManager.getCellBroadcastChannelRanges( + R.array.additional_cbs_channels_strings); + + for (CellBroadcastChannelRange range : ranges) { + if (range.mStartId <= channel && range.mEndId >= channel) { + // Check if the channel is within the scope. If not, ignore the alert message. + if (!channelManager.checkScope(range.mScope)) { + Log.d(TAG, "The range [" + range.mStartId + "-" + range.mEndId + + "] is not within the scope. mScope = " + range.mScope); + return false; + } - // The area update information cell broadcast should not cause any pop-up. - // Instead the setting's app SIM status will show its information. - if (range.mAlertType == AlertType.AREA) { - if (enableAreaUpdateInfoAlerts) { - // save latest area info broadcast for Settings display and send as - // broadcast. - CellBroadcastReceiverApp.setLatestAreaInfo(message); - Intent intent = new Intent(CB_AREA_INFO_RECEIVED_ACTION); - intent.setPackage(SETTINGS_APP); - intent.putExtra(EXTRA_MESSAGE, message); - // Send broadcast twice, once for apps that have PRIVILEGED permission - // and once for those that have the runtime one. - sendBroadcastAsUser(intent, UserHandle.ALL, - android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE); - sendBroadcastAsUser(intent, UserHandle.ALL, - android.Manifest.permission.READ_PHONE_STATE); - // area info broadcasts are displayed in Settings status screen - } - return false; - } else if (range.mAlertType == AlertType.TEST) { - return emergencyAlertEnabled - && PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS, - false); + // The area update information cell broadcast should not cause any pop-up. + // Instead the setting's app SIM status will show its information. + if (range.mAlertType == AlertType.AREA) { + if (enableAreaUpdateInfoAlerts) { + // save latest area info broadcast for Settings display and send as + // broadcast. + CellBroadcastReceiverApp.setLatestAreaInfo(message); + Intent intent = new Intent(CB_AREA_INFO_RECEIVED_ACTION); + intent.setPackage(SETTINGS_APP); + intent.putExtra(EXTRA_MESSAGE, message.getSmsCbMessage()); + // Send broadcast twice, once for apps that have PRIVILEGED permission + // and once for those that have the runtime one. + sendBroadcastAsUser(intent, UserHandle.ALL, + android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE); + sendBroadcastAsUser(intent, UserHandle.ALL, + android.Manifest.permission.READ_PHONE_STATE); + // area info broadcasts are displayed in Settings status screen } - - return emergencyAlertEnabled; + return false; + } else if (range.mAlertType == AlertType.TEST) { + return emergencyAlertEnabled + && PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS, + false); } + + return emergencyAlertEnabled; } } - int subId = message.getSubId(); - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.emergency_alerts_channels_range_strings, this)) { + + if (channelManager.checkCellBroadcastChannelRange(channel, + R.array.emergency_alerts_channels_range_strings)) { return emergencyAlertEnabled && PreferenceManager.getDefaultSharedPreferences(this).getBoolean( CellBroadcastSettings.KEY_ENABLE_EMERGENCY_ALERTS, true); } // CMAS warning types - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.cmas_presidential_alerts_channels_range_strings, this)) { + if (channelManager.checkCellBroadcastChannelRange(channel, + R.array.cmas_presidential_alerts_channels_range_strings)) { // always enabled return true; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.cmas_alert_extreme_channels_range_strings, this)) { + if (channelManager.checkCellBroadcastChannelRange(channel, + R.array.cmas_alert_extreme_channels_range_strings)) { return emergencyAlertEnabled && PreferenceManager.getDefaultSharedPreferences(this).getBoolean( CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true); } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.cmas_alerts_severe_range_strings, this)) { + if (channelManager.checkCellBroadcastChannelRange(channel, + R.array.cmas_alerts_severe_range_strings)) { return emergencyAlertEnabled && PreferenceManager.getDefaultSharedPreferences(this).getBoolean( CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true); } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.cmas_amber_alerts_channels_range_strings, this)) { + if (channelManager.checkCellBroadcastChannelRange(channel, + R.array.cmas_amber_alerts_channels_range_strings)) { return emergencyAlertEnabled && PreferenceManager.getDefaultSharedPreferences(this) .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true); } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.required_monthly_test_range_strings, this) - || CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.exercise_alert_range_strings, this) - || CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.operator_defined_alert_range_strings, this)) { + if (channelManager.checkCellBroadcastChannelRange(channel, + R.array.required_monthly_test_range_strings) + || channelManager.checkCellBroadcastChannelRange(channel, + R.array.exercise_alert_range_strings) + || channelManager.checkCellBroadcastChannelRange(channel, + R.array.operator_defined_alert_range_strings)) { return emergencyAlertEnabled && PreferenceManager.getDefaultSharedPreferences(this) .getBoolean(CellBroadcastSettings.KEY_ENABLE_TEST_ALERTS, false); } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.public_safety_messages_channels_range_strings, this)) { + + if (channelManager.checkCellBroadcastChannelRange(channel, + R.array.public_safety_messages_channels_range_strings)) { return emergencyAlertEnabled && PreferenceManager.getDefaultSharedPreferences(this) .getBoolean(CellBroadcastSettings.KEY_ENABLE_PUBLIC_SAFETY_MESSAGES, true); } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - channel, R.array.state_local_test_alert_range_strings, this)) { + + if (channelManager.checkCellBroadcastChannelRange(channel, + R.array.state_local_test_alert_range_strings)) { return emergencyAlertEnabled && PreferenceManager.getDefaultSharedPreferences(this) .getBoolean(CellBroadcastSettings.KEY_ENABLE_STATE_LOCAL_TEST_ALERTS, false); } + return true; } @@ -584,6 +582,9 @@ public class CellBroadcastAlertService extends Service { audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + mContext, message.getSubId(mContext)); + AlertType alertType = AlertType.DEFAULT; if (message.isEtwsMessage()) { alertType = AlertType.ETWS_DEFAULT; @@ -609,27 +610,31 @@ public class CellBroadcastAlertService extends Service { } } else { int channel = message.getServiceCategory(); - ArrayList<CellBroadcastChannelRange> ranges = CellBroadcastChannelManager - .getAllCellBroadcastChannelRanges(getApplicationContext()); - if (ranges != null) { - for (CellBroadcastChannelRange range : ranges) { - if (channel >= range.mStartId && channel <= range.mEndId) { - alertType = range.mAlertType; - break; - } + ArrayList<CellBroadcastChannelRange> ranges = channelManager + .getAllCellBroadcastChannelRanges(); + for (CellBroadcastChannelRange range : ranges) { + if (channel >= range.mStartId && channel <= range.mEndId) { + alertType = range.mAlertType; + break; } } } - CellBroadcastChannelRange range = CellBroadcastChannelManager - .getCellBroadcastChannelRangeFromMessage(getApplicationContext(), message); + CellBroadcastChannelRange range = channelManager + .getCellBroadcastChannelRangeFromMessage(message); audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_TONE_TYPE, alertType); audioIntent.putExtra( CellBroadcastAlertAudio.ALERT_AUDIO_VIBRATION_PATTERN_EXTRA, (range != null) ? range.mVibrationPattern - : CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId( - getApplicationContext()) - .getIntArray(R.array.default_vibration_pattern)); + : CellBroadcastSettings.getResources(mContext, message.getSubId(mContext)) + .getIntArray(R.array.default_vibration_pattern)); + + Resources res = CellBroadcastSettings.getResources(mContext, message.getSubId(mContext)); + if ((res.getBoolean(R.bool.full_volume_presidential_alert) + && message.getCmasMessageClass() == SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT) + || prefs.getBoolean(CellBroadcastSettings.KEY_USE_FULL_VOLUME, false)) { + audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_FULL_VOLUME_EXTRA, true); + } String messageBody = message.getMessageBody(); @@ -642,6 +647,9 @@ public class CellBroadcastAlertService extends Service { audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE, language); } + + audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_SUB_INDEX, + message.getSubId(mContext)); startService(audioIntent); ArrayList<CellBroadcastMessage> messageList = new ArrayList<CellBroadcastMessage>(1); @@ -667,7 +675,7 @@ public class CellBroadcastAlertService extends Service { static void addToNotificationBar(CellBroadcastMessage message, ArrayList<CellBroadcastMessage> messageList, Context context, boolean fromSaveState) { - Resources res = CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context); + Resources res = CellBroadcastSettings.getResources(context, message.getSubId(context)); int channelTitleId = CellBroadcastResources.getDialogTitleResource(context, message); CharSequence channelName = context.getText(channelTitleId); String messageBody = message.getMessageBody(); @@ -696,7 +704,10 @@ public class CellBroadcastAlertService extends Service { pi = PendingIntent.getActivity(context, NOTIFICATION_ID, intent, PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); } - final String channelId = CellBroadcastChannelManager.isEmergencyMessage(context, message) + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + context, message.getSubId(context)); + + final String channelId = channelManager.isEmergencyMessage(message) ? NOTIFICATION_CHANNEL_EMERGENCY_ALERTS : NOTIFICATION_CHANNEL_NON_EMERGENCY_ALERTS; boolean nonSwipeableNotification = message.isEmergencyAlertMessage() @@ -745,7 +756,7 @@ public class CellBroadcastAlertService extends Service { // addToNotification for the emergency display on FEATURE WATCH devices vs the // Alert Dialog, it will call this and override the emergency audio tone. if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH) - && !CellBroadcastChannelManager.isEmergencyMessage(context, message)) { + && !channelManager.isEmergencyMessage(message)) { if (res.getBoolean(R.bool.watch_enable_non_emergency_audio)) { // start audio/vibration/speech service for non emergency alerts Intent audioIntent = new Intent(context, CellBroadcastAlertAudio.class); diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAreaInfoReceiver.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAreaInfoReceiver.java index d1a9a2df1..51bdc4c80 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAreaInfoReceiver.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAreaInfoReceiver.java @@ -20,7 +20,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.UserHandle; -import android.telephony.CellBroadcastMessage; import android.util.Log; public class CellBroadcastAreaInfoReceiver extends BroadcastReceiver { diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastChannelManager.java b/src/com/android/cellbroadcastreceiver/CellBroadcastChannelManager.java index fcc51d3f8..a41a02eb6 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastChannelManager.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastChannelManager.java @@ -18,9 +18,9 @@ package com.android.cellbroadcastreceiver; import static android.telephony.ServiceState.ROAMING_TYPE_NOT_ROAMING; +import android.annotation.NonNull; import android.content.Context; import android.telephony.AccessNetworkConstants; -import android.telephony.CellBroadcastMessage; import android.telephony.NetworkRegistrationInfo; import android.telephony.ServiceState; import android.telephony.SmsManager; @@ -69,6 +69,10 @@ public class CellBroadcastChannelManager { private static ArrayList<CellBroadcastChannelRange> sAllCellBroadcastChannelRanges = null; + private final Context mContext; + + private final int mSubId; + /** * Cell broadcast channel range * A range is consisted by starting channel id, ending channel id, and the alert type @@ -109,14 +113,14 @@ public class CellBroadcastChannelManager { public int[] mVibrationPattern; public boolean mFilterLanguage; - public CellBroadcastChannelRange(Context context, String channelRange) throws Exception { + public CellBroadcastChannelRange(Context context, int subId, String channelRange) { mAlertType = AlertType.DEFAULT; mEmergencyLevel = LEVEL_UNKNOWN; mRat = SmsManager.CELL_BROADCAST_RAN_TYPE_GSM; mScope = SCOPE_UNKNOWN; mVibrationPattern = - CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context) + CellBroadcastSettings.getResources(context, subId) .getIntArray(R.array.default_vibration_pattern); mFilterLanguage = false; @@ -130,7 +134,6 @@ public class CellBroadcastChannelManager { if (tokens.length == 2) { String key = tokens[0].trim(); String value = tokens[1].trim(); - if (value == null) continue; switch (key) { case KEY_TYPE: mAlertType = AlertType.valueOf(value.toUpperCase()); @@ -158,7 +161,7 @@ public class CellBroadcastChannelManager { break; case KEY_VIBRATION: String[] vibration = value.split("\\|"); - if (vibration != null && vibration.length > 0) { + if (vibration.length > 0) { mVibrationPattern = new int[vibration.length]; for (int i = 0; i < vibration.length; i++) { mVibrationPattern[i] = Integer.parseInt(vibration[i]); @@ -197,25 +200,33 @@ public class CellBroadcastChannelManager { } /** + * Constructor + * + * @param context Context + * @param subId Subscription index + */ + public CellBroadcastChannelManager(Context context, int subId) { + mContext = context; + mSubId = subId; + } + + /** * Get cell broadcast channels enabled by the carriers from resource key - * @param context Application context + * * @param key Resource key + * * @return The list of channel ranges enabled by the carriers. */ - public static ArrayList<CellBroadcastChannelRange> getCellBroadcastChannelRanges( - Context context, int key) { + public @NonNull ArrayList<CellBroadcastChannelRange> getCellBroadcastChannelRanges(int key) { ArrayList<CellBroadcastChannelRange> result = new ArrayList<>(); String[] ranges = - CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context) - .getStringArray(key); + CellBroadcastSettings.getResources(mContext, mSubId).getStringArray(key); - if (ranges != null) { - for (String range : ranges) { - try { - result.add(new CellBroadcastChannelRange(context, range)); - } catch (Exception e) { - loge("Failed to parse \"" + range + "\". e=" + e); - } + for (String range : ranges) { + try { + result.add(new CellBroadcastChannelRange(mContext, mSubId, range)); + } catch (Exception e) { + loge("Failed to parse \"" + range + "\". e=" + e); } } @@ -225,17 +236,15 @@ public class CellBroadcastChannelManager { /** * Get all cell broadcast channels * - * @param context Application context * @return all cell broadcast channels */ - public static ArrayList<CellBroadcastChannelRange> getAllCellBroadcastChannelRanges( - Context context) { + public @NonNull ArrayList<CellBroadcastChannelRange> getAllCellBroadcastChannelRanges() { if (sAllCellBroadcastChannelRanges != null) return sAllCellBroadcastChannelRanges; ArrayList<CellBroadcastChannelRange> result = new ArrayList<>(); for (int key : sCellBroadcastRangeResourceKeys) { - result.addAll(getCellBroadcastChannelRanges(context, key)); + result.addAll(getCellBroadcastChannelRanges(key)); } sAllCellBroadcastChannelRanges = result; @@ -243,64 +252,57 @@ public class CellBroadcastChannelManager { } /** - * @param subId Subscription index * @param channel Cell broadcast message channel - * @param context Application context * @param key Resource key + * * @return {@code TRUE} if the input channel is within the channel range defined from resource. * return {@code FALSE} otherwise */ - public static boolean checkCellBroadcastChannelRange(int subId, int channel, int key, - Context context) { - ArrayList<CellBroadcastChannelRange> ranges = - CellBroadcastChannelManager.getCellBroadcastChannelRanges(context, key); - if (ranges != null) { - for (CellBroadcastChannelRange range : ranges) { - if (channel >= range.mStartId && channel <= range.mEndId) { - return checkScope(context, subId, range.mScope); - } + public boolean checkCellBroadcastChannelRange(int channel, int key) { + ArrayList<CellBroadcastChannelRange> ranges = getCellBroadcastChannelRanges(key); + + for (CellBroadcastChannelRange range : ranges) { + if (channel >= range.mStartId && channel <= range.mEndId) { + return checkScope(range.mScope); } } + return false; } /** * Check if the channel scope matches the current network condition. * - * @param subId Subscription id * @param rangeScope Range scope. Must be SCOPE_CARRIER, SCOPE_DOMESTIC, or SCOPE_INTERNATIONAL. * @return True if the scope matches the current network roaming condition. */ - public static boolean checkScope(Context context, int subId, int rangeScope) { + public boolean checkScope(int rangeScope) { if (rangeScope == CellBroadcastChannelRange.SCOPE_UNKNOWN) return true; - if (context != null) { - TelephonyManager tm = - (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - tm = tm.createForSubscriptionId(subId); - ServiceState ss = tm.getServiceState(); - if (ss != null) { - NetworkRegistrationInfo regInfo = ss.getNetworkRegistrationInfo( - NetworkRegistrationInfo.DOMAIN_CS, - AccessNetworkConstants.TRANSPORT_TYPE_WWAN); - if (regInfo != null) { - if (regInfo.getRegistrationState() == - NetworkRegistrationInfo.REGISTRATION_STATE_HOME - || regInfo.getRegistrationState() == - NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING - || regInfo.isEmergencyEnabled()) { - int voiceRoamingType = (regInfo != null) ? regInfo.getRoamingType() : - ROAMING_TYPE_NOT_ROAMING; - if (voiceRoamingType == ROAMING_TYPE_NOT_ROAMING) { - return true; - } else if (voiceRoamingType == ServiceState.ROAMING_TYPE_DOMESTIC - && rangeScope == CellBroadcastChannelRange.SCOPE_DOMESTIC) { - return true; - } else if (voiceRoamingType == ServiceState.ROAMING_TYPE_INTERNATIONAL - && rangeScope == CellBroadcastChannelRange.SCOPE_INTERNATIONAL) { - return true; - } - return false; + TelephonyManager tm = + (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + tm = tm.createForSubscriptionId(mSubId); + ServiceState ss = tm.getServiceState(); + if (ss != null) { + NetworkRegistrationInfo regInfo = ss.getNetworkRegistrationInfo( + NetworkRegistrationInfo.DOMAIN_CS, + AccessNetworkConstants.TRANSPORT_TYPE_WWAN); + if (regInfo != null) { + if (regInfo.getRegistrationState() + == NetworkRegistrationInfo.REGISTRATION_STATE_HOME + || regInfo.getRegistrationState() + == NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING + || regInfo.isEmergencyEnabled()) { + int voiceRoamingType = regInfo.getRoamingType(); + if (voiceRoamingType == ROAMING_TYPE_NOT_ROAMING) { + return true; + } else if (voiceRoamingType == ServiceState.ROAMING_TYPE_DOMESTIC + && rangeScope == CellBroadcastChannelRange.SCOPE_DOMESTIC) { + return true; + } else if (voiceRoamingType == ServiceState.ROAMING_TYPE_INTERNATIONAL + && rangeScope == CellBroadcastChannelRange.SCOPE_INTERNATIONAL) { + return true; } + return false; } } } @@ -310,18 +312,23 @@ public class CellBroadcastChannelManager { /** * Return corresponding cellbroadcast range where message belong to - * @param context Application context + * * @param message Cell broadcast message */ - public static CellBroadcastChannelRange getCellBroadcastChannelRangeFromMessage( - Context context, CellBroadcastMessage message) { - int subId = message.getSubId(); + public CellBroadcastChannelRange getCellBroadcastChannelRangeFromMessage( + CellBroadcastMessage message) { + if (mSubId != message.getSubId(mContext)) { + Log.e(TAG, "getCellBroadcastChannelRangeFromMessage: This manager is created for " + + "sub " + mSubId + ", should not be used for message from sub " + + message.getSubId(mContext)); + } + int channel = message.getServiceCategory(); ArrayList<CellBroadcastChannelRange> ranges = null; for (int key : sCellBroadcastRangeResourceKeys) { - if (checkCellBroadcastChannelRange(subId, channel, key, context)) { - ranges = getCellBroadcastChannelRanges(context, key); + if (checkCellBroadcastChannelRange(channel, key)) { + ranges = getCellBroadcastChannelRanges(key); break; } } @@ -338,20 +345,25 @@ public class CellBroadcastChannelManager { /** * Check if the cell broadcast message is an emergency message or not - * @param context Application context + * * @param message Cell broadcast message * @return True if the message is an emergency message, otherwise false. */ - public static boolean isEmergencyMessage(Context context, CellBroadcastMessage message) { + public boolean isEmergencyMessage(CellBroadcastMessage message) { if (message == null) { return false; } + if (mSubId != message.getSubId(mContext)) { + Log.e(TAG, "This manager is created for sub " + mSubId + + ", should not be used for message from sub " + message.getSubId(mContext)); + } + int id = message.getServiceCategory(); for (int key : sCellBroadcastRangeResourceKeys) { ArrayList<CellBroadcastChannelRange> ranges = - getCellBroadcastChannelRanges(context, key); + getCellBroadcastChannelRanges(key); for (CellBroadcastChannelRange range : ranges) { if (range.mStartId <= id && range.mEndId >= id) { switch (range.mEmergencyLevel) { diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java b/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java index bea2de515..ac5503310 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java @@ -19,6 +19,7 @@ package com.android.cellbroadcastreceiver; import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.VDBG; import android.app.IntentService; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; @@ -59,52 +60,22 @@ public class CellBroadcastConfigService extends IntentService { protected void onHandleIntent(Intent intent) { if (ACTION_ENABLE_CHANNELS.equals(intent.getAction())) { try { - - SubscriptionManager subManager = SubscriptionManager.from(getApplicationContext()); - int subId = SubscriptionManager.getDefaultSmsSubscriptionId(); - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - subId = SubscriptionManager.getDefaultSubscriptionId(); - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID && - subManager != null) { - int [] subIds = getActiveSubIdList(subManager); - if (subIds.length != 0) { - subId = subIds[0]; - } - } - } + SubscriptionManager subManager = (SubscriptionManager) getApplicationContext() + .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE); if (subManager != null) { - // Retrieve all the active sub ids. We only want to enable - // cell broadcast on the sub we are interested in and we'll disable - // it on other subs so the users will not receive duplicate messages from - // multiple carriers (e.g. for multi-sim users). + // Retrieve all the active subscription indice and enable cell broadcast + // messages on all subs. The duplication detection will be done at the + // frameworks. int [] subIds = getActiveSubIdList(subManager); - if (subIds.length != 0) - { - for (int id : subIds) { - SmsManager manager = SmsManager.getSmsManagerForSubscriptionId(id); - if (manager != null) { - if (id == subId) { - // Enable cell broadcast messages on this sub. - log("Enable CellBroadcast on sub " + id); - setCellBroadcastOnSub(manager, true); - } - else { - // Disable all cell broadcast message on this sub. - // This is only for multi-sim scenario. For single SIM device - // we should not reach here. - log("Disable CellBroadcast on sub " + id); - setCellBroadcastOnSub(manager, false); - } - } + if (subIds.length != 0) { + for (int subId : subIds) { + log("Enable CellBroadcast on sub " + subId); + enableCellBroadcastChannels(subId); } - } - else { + } else { // For no sim scenario. - SmsManager manager = SmsManager.getDefault(); - if (manager != null) { - setCellBroadcastOnSub(manager, true); - } + enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); } } } catch (Exception ex) { @@ -123,14 +94,13 @@ public class CellBroadcastConfigService extends IntentService { } /** - * Enable/disable cell broadcast messages id on one subscription - * This includes all ETWS and CMAS alerts. - * @param manager SMS manager - * @param enableForSub True if want to enable messages on this sub (e.g default SMS). False - * will disable all messages + * Enable cell broadcast messages channels. Messages can be only received on the + * enabled channels. + * + * @param subId Subscription index */ @VisibleForTesting - public void setCellBroadcastOnSub(SmsManager manager, boolean enableForSub) { + public void enableCellBroadcastChannels(int subId) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); @@ -138,14 +108,14 @@ public class CellBroadcastConfigService extends IntentService { // Note: If enableAlertsMasterToggle is false, it disables ALL emergency broadcasts // except for CMAS presidential. i.e. to receive CMAS severe alerts, both // enableAlertsMasterToggle AND enableCmasSevereAlerts must be true. - boolean enableAlertsMasterToggle = enableForSub && prefs.getBoolean( + boolean enableAlertsMasterToggle = prefs.getBoolean( CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); boolean enableEtwsAlerts = enableAlertsMasterToggle; // CMAS Presidential must be always on (See 3GPP TS 22.268 Section 6.2) regardless // user's preference - boolean enablePresidential = enableForSub; + boolean enablePresidential = true; boolean enableCmasExtremeAlerts = enableAlertsMasterToggle && prefs.getBoolean( CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true); @@ -174,7 +144,7 @@ public class CellBroadcastConfigService extends IntentService { boolean enableEmergencyAlerts = enableAlertsMasterToggle && prefs.getBoolean( CellBroadcastSettings.KEY_ENABLE_EMERGENCY_ALERTS, true); - boolean enableGeoFencingTriggerMessage = enableForSub; + boolean enableGeoFencingTriggerMessage = true; if (VDBG) { log("enableAlertsMasterToggle = " + enableAlertsMasterToggle); @@ -192,124 +162,108 @@ public class CellBroadcastConfigService extends IntentService { log("enableGeoFencingTriggerMessage = " + enableGeoFencingTriggerMessage); } + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + getApplicationContext(), subId); + /** Enable CMAS series messages. */ // Enable/Disable Presidential messages. - setCellBroadcastRange( - manager, - enablePresidential, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.cmas_presidential_alerts_channels_range_strings)); + setCellBroadcastRange(subId, enablePresidential, + channelManager.getCellBroadcastChannelRanges( + R.array.cmas_presidential_alerts_channels_range_strings)); // Enable/Disable CMAS extreme messages. - setCellBroadcastRange( - manager, - enableCmasExtremeAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.cmas_alert_extreme_channels_range_strings)); + setCellBroadcastRange(subId, enableCmasExtremeAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.cmas_alert_extreme_channels_range_strings)); // Enable/Disable CMAS severe messages. - setCellBroadcastRange( - manager, - enableCmasSevereAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.cmas_alerts_severe_range_strings)); + setCellBroadcastRange(subId, enableCmasSevereAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.cmas_alerts_severe_range_strings)); // Enable/Disable CMAS amber alert messages. - setCellBroadcastRange( - manager, - enableCmasAmberAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.cmas_amber_alerts_channels_range_strings)); + setCellBroadcastRange(subId, enableCmasAmberAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.cmas_amber_alerts_channels_range_strings)); // Enable/Disable test messages. - setCellBroadcastRange( - manager, - enableTestAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.required_monthly_test_range_strings)); - setCellBroadcastRange( - manager, - enableTestAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.exercise_alert_range_strings)); - setCellBroadcastRange( - manager, - enableTestAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.operator_defined_alert_range_strings)); + setCellBroadcastRange(subId, enableTestAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.required_monthly_test_range_strings)); + + setCellBroadcastRange(subId, enableTestAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.exercise_alert_range_strings)); + setCellBroadcastRange(subId, enableTestAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.operator_defined_alert_range_strings)); // Enable/Disable GSM ETWS messages. - setCellBroadcastRange( - manager, - enableEtwsAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.etws_alerts_range_strings)); + setCellBroadcastRange(subId, enableEtwsAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.etws_alerts_range_strings)); // Enable/Disable GSM ETWS test messages. - setCellBroadcastRange( - manager, - enableTestAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.etws_test_alerts_range_strings)); + setCellBroadcastRange(subId, enableTestAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.etws_test_alerts_range_strings)); // Enable/Disable GSM public safety messages. - setCellBroadcastRange( - manager, - enablePublicSafetyMessagesChannelAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.public_safety_messages_channels_range_strings)); + setCellBroadcastRange(subId, enablePublicSafetyMessagesChannelAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.public_safety_messages_channels_range_strings)); // Enable/Disable GSM state/local test alerts. - setCellBroadcastRange( - manager, - enableStateLocalTestAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.state_local_test_alert_range_strings)); + setCellBroadcastRange(subId, enableStateLocalTestAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.state_local_test_alert_range_strings)); // Enable/Disable GSM geo-fencing trigger messages. - setCellBroadcastRange( - manager, - enableGeoFencingTriggerMessage, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.geo_fencing_trigger_messages_range_strings)); + setCellBroadcastRange(subId, enableGeoFencingTriggerMessage, + channelManager.getCellBroadcastChannelRanges( + R.array.geo_fencing_trigger_messages_range_strings)); /** Enable non-CMAS series messages. */ - setCellBroadcastRange( - manager, - enableEmergencyAlerts, - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.emergency_alerts_channels_range_strings)); + setCellBroadcastRange(subId, enableEmergencyAlerts, + channelManager.getCellBroadcastChannelRanges( + R.array.emergency_alerts_channels_range_strings)); // Enable/Disable additional channels based on carrier specific requirement. ArrayList<CellBroadcastChannelRange> ranges = - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this, R.array.additional_cbs_channels_strings); - if (ranges != null) { - for (CellBroadcastChannelRange range: ranges) { - boolean enableAlerts; - switch (range.mAlertType) { - case AREA: - enableAlerts = enableAreaUpdateInfoAlerts; - break; - case TEST: - enableAlerts = enableTestAlerts; - break; - default: - enableAlerts = enableAlertsMasterToggle; - } - setCellBroadcastRange(manager, enableAlerts, new ArrayList<>(Arrays.asList(range))); + channelManager.getCellBroadcastChannelRanges( + R.array.additional_cbs_channels_strings); + + for (CellBroadcastChannelRange range: ranges) { + boolean enableAlerts; + switch (range.mAlertType) { + case AREA: + enableAlerts = enableAreaUpdateInfoAlerts; + break; + case TEST: + enableAlerts = enableTestAlerts; + break; + default: + enableAlerts = enableAlertsMasterToggle; } + setCellBroadcastRange(subId, enableAlerts, new ArrayList<>(Arrays.asList(range))); } } /** * Enable/disable cell broadcast with messages id range - * @param manager SMS manager + * @param subId Subscription index * @param enable True for enabling cell broadcast with id range, otherwise for disabling. * @param ranges Cell broadcast id ranges */ - private void setCellBroadcastRange( - SmsManager manager, boolean enable, List<CellBroadcastChannelRange> ranges) { + private void setCellBroadcastRange(int subId, boolean enable, + List<CellBroadcastChannelRange> ranges) { + SmsManager manager; + if (subId != SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { + manager = SmsManager.getSmsManagerForSubscriptionId(subId); + } else { + manager = SmsManager.getDefault(); + } + if (ranges != null) { for (CellBroadcastChannelRange range: ranges) { if (enable) { diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastContentProvider.java b/src/com/android/cellbroadcastreceiver/CellBroadcastContentProvider.java index d9b2a3f59..c2eae7a05 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastContentProvider.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastContentProvider.java @@ -29,7 +29,6 @@ import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.AsyncTask; import android.provider.Telephony; -import android.telephony.CellBroadcastMessage; import android.text.TextUtils; import android.util.Log; @@ -196,7 +195,6 @@ public class CellBroadcastContentProvider extends ContentProvider { // is not compatible with CMAS carrier requirements and could also cause other emergency // alerts, e.g. ETWS, to not display if the database is filled with old messages. // Use duplicate message ID detection in CellBroadcastAlertService instead of DB query. - long rowId = db.insert(CellBroadcastDatabaseHelper.TABLE_NAME, null, cv); if (rowId == -1) { Log.e(TAG, "failed to insert new broadcast into database"); diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastCursorAdapter.java b/src/com/android/cellbroadcastreceiver/CellBroadcastCursorAdapter.java index d10f63bb6..9641c64fc 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastCursorAdapter.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastCursorAdapter.java @@ -18,7 +18,6 @@ package com.android.cellbroadcastreceiver; import android.content.Context; import android.database.Cursor; -import android.telephony.CellBroadcastMessage; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastDatabaseHelper.java b/src/com/android/cellbroadcastreceiver/CellBroadcastDatabaseHelper.java index 4b76ad065..84f8598bb 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastDatabaseHelper.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastDatabaseHelper.java @@ -55,8 +55,9 @@ public class CellBroadcastDatabaseHelper extends SQLiteOpenHelper { * Database version 2-9: (reserved for OEM database customization) * Database version 10: adds ETWS and CMAS columns and CDMA support * Database version 11: adds delivery time index + * Datatbase version 12: add slotIndex */ - static final int DATABASE_VERSION = 11; + static final int DATABASE_VERSION = 12; CellBroadcastDatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -66,6 +67,7 @@ public class CellBroadcastDatabaseHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + TABLE_NAME + " (" + Telephony.CellBroadcasts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + Telephony.CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0," + Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE + " INTEGER," + Telephony.CellBroadcasts.PLMN + " TEXT," + Telephony.CellBroadcasts.LAC + " INTEGER," @@ -165,6 +167,11 @@ public class CellBroadcastDatabaseHelper extends SQLiteOpenHelper { createDeliveryTimeIndex(db); oldVersion++; } + + if (oldVersion == 11) { + db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + + Telephony.CellBroadcasts.SLOT_INDEX + " INTEGER DEFAULT 0;"); + } } /** diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastHandler.java b/src/com/android/cellbroadcastreceiver/CellBroadcastHandler.java deleted file mode 100644 index a37d35b23..000000000 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastHandler.java +++ /dev/null @@ -1,442 +0,0 @@ -/* - * Copyright (C) 2013 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.cellbroadcastreceiver; - -import static android.content.PermissionChecker.PERMISSION_GRANTED; -import static android.provider.Settings.Secure.CMAS_ADDITIONAL_BROADCAST_PKG; - -import android.Manifest; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.Activity; -import android.app.AppOpsManager; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.PermissionChecker; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -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.text.format.DateUtils; -import android.util.LocalLog; -import android.util.Log; - -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.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Dispatch new Cell Broadcasts to receivers. Acquires a private wakelock until the broadcast - * completes and our result receiver is called. - */ -public class CellBroadcastHandler extends WakeLockStateMachine { - private static final String EXTRA_MESSAGE = "message"; - - private final LocalLog mLocalLog = new LocalLog(100); - - protected static final Uri CELL_BROADCAST_URI = Uri.parse("content://cellbroadcasts_fwk"); - - /** Uses to request the location update. */ - public final LocationRequester mLocationRequester; - - private CellBroadcastHandler(Context context) { - this("CellBroadcastHandler", context); - } - - protected CellBroadcastHandler(String debugTag, Context context) { - super(debugTag, context); - mLocationRequester = new LocationRequester( - context, - (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE), - getHandler().getLooper()); - } - - /** - * Create a new CellBroadcastHandler. - * @param context the context to use for dispatching Intents - * @return the new handler - */ - public static CellBroadcastHandler makeCellBroadcastHandler(Context context) { - CellBroadcastHandler handler = new CellBroadcastHandler(context); - handler.start(); - return handler; - } - - /** - * Handle Cell Broadcast messages from {@code CdmaInboundSmsHandler}. - * 3GPP-format Cell Broadcast messages sent from radio are handled in the subclass. - * - * @param message the message to process - * @return true if need to wait for geo-fencing or an ordered broadcast was sent. - */ - @Override - protected boolean handleSmsMessage(Message message) { - if (message.obj instanceof SmsCbMessage) { - handleBroadcastSms((SmsCbMessage) message.obj, message.arg1); - return true; - } else { - loge("handleMessage got object of type: " + message.obj.getClass().getName()); - return false; - } - } - - /** - * Dispatch a Cell Broadcast message to listeners. - * @param message the Cell Broadcast to broadcast - */ - protected void handleBroadcastSms(SmsCbMessage message, int slotIndex) { - // Log Cellbroadcast msg received event - TelephonyMetrics metrics = TelephonyMetrics.getInstance(); - metrics.writeNewCBSms(slotIndex, message.getMessageFormat(), - message.getMessagePriority(), message.isCmasMessage(), message.isEtwsMessage(), - message.getServiceCategory(), message.getSerialNumber(), - System.currentTimeMillis()); - - // 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); - - if (message.needGeoFencingCheck()) { - if (DBG) { - log("Request location update for geo-fencing. serialNumber = " - + message.getSerialNumber()); - } - - requestLocationUpdate(location -> { - if (location == null) { - // Broadcast the message directly if the location is not available. - broadcastMessage(message, uri, slotIndex); - } else { - performGeoFencing(message, uri, message.getGeometries(), location, slotIndex); - } - }, message.getMaximumWaitingTime()); - } else { - if (DBG) { - log("Broadcast the message directly because no geo-fencing required, " - + "serialNumber = " + message.getSerialNumber() - + " needGeoFencing = " + message.needGeoFencingCheck()); - } - broadcastMessage(message, uri, slotIndex); - } - } - - /** - * 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 - * @param uri the message's uri - * @param broadcastArea the broadcast area of the message - * @param location current location - */ - protected void performGeoFencing(SmsCbMessage message, Uri uri, List<Geometry> broadcastArea, - LatLng location, int slotIndex) { - - if (DBG) { - logd("Perform geo-fencing check for message identifier = " - + message.getServiceCategory() - + " serialNumber = " + message.getSerialNumber()); - } - - for (Geometry geo : broadcastArea) { - if (geo.contains(location)) { - broadcastMessage(message, uri, slotIndex); - return; - } - } - - if (DBG) { - logd("Device location is outside the broadcast area " - + CbGeoUtils.encodeGeometriesToString(broadcastArea)); - } - } - - /** - * Request a single location update. - * @param callback a callback will be called when the location is available. - * @param maximumWaitTimeSec the maximum wait time of this request. If location is not updated - * within the maximum wait time, {@code callback#onLocationUpadte(null)} will be called. - */ - protected void requestLocationUpdate(LocationUpdateCallback callback, int maximumWaitTimeSec) { - mLocationRequester.requestLocationUpdate(callback, maximumWaitTimeSec); - } - - /** - * 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 - */ - protected void broadcastMessage(@NonNull SmsCbMessage message, @Nullable Uri messageUri, - int slotIndex) { - String receiverPermission; - int appOp; - String msg; - Intent intent; - if (message.isEmergencyMessage()) { - msg = "Dispatching emergency SMS CB, SmsCbMessage is: " + message; - log(msg); - mLocalLog.log(msg); - intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); - //Emergency alerts need to be delivered with high priority - intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - receiverPermission = Manifest.permission.RECEIVE_EMERGENCY_BROADCAST; - appOp = AppOpsManager.OP_RECEIVE_EMERGECY_SMS; - - intent.putExtra(EXTRA_MESSAGE, message); - SubscriptionManager.putPhoneIdAndSubIdExtra(intent, slotIndex); - - if (Build.IS_DEBUGGABLE) { - // Send additional broadcast intent to the specified package. This is only for sl4a - // automation tests. - final String additionalPackage = Settings.Secure.getString( - mContext.getContentResolver(), CMAS_ADDITIONAL_BROADCAST_PKG); - if (additionalPackage != null) { - Intent additionalIntent = new Intent(intent); - additionalIntent.setPackage(additionalPackage); - mContext.sendOrderedBroadcastAsUser(additionalIntent, UserHandle.ALL, - receiverPermission, appOp, null, getHandler(), Activity.RESULT_OK, - null, null); - } - } - - String[] pkgs = mContext.getResources().getStringArray( - com.android.internal.R.array.config_defaultCellBroadcastReceiverPkgs); - mReceiverCount.addAndGet(pkgs.length); - for (String pkg : pkgs) { - // Explicitly send the intent to all the configured cell broadcast receivers. - intent.setPackage(pkg); - mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission, - appOp, mReceiver, getHandler(), Activity.RESULT_OK, null, null); - } - } else { - msg = "Dispatching SMS CB, SmsCbMessage is: " + message; - log(msg); - mLocalLog.log(msg); - intent = new Intent(Telephony.Sms.Intents.SMS_CB_RECEIVED_ACTION); - // Send implicit intent since there are various 3rd party carrier apps listen to - // this intent. - intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); - receiverPermission = Manifest.permission.RECEIVE_SMS; - appOp = AppOpsManager.OP_RECEIVE_SMS; - - intent.putExtra(EXTRA_MESSAGE, message); - SubscriptionManager.putPhoneIdAndSubIdExtra(intent, slotIndex); - - mReceiverCount.incrementAndGet(); - mContext.sendOrderedBroadcastAsUser(intent, UserHandle.ALL, receiverPermission, appOp, - mReceiver, getHandler(), Activity.RESULT_OK, null, null); - } - - if (messageUri != null) { - ContentValues cv = new ContentValues(); - cv.put(CellBroadcasts.MESSAGE_BROADCASTED, 1); - mContext.getContentResolver().update(CELL_BROADCAST_URI, cv, - CellBroadcasts._ID + "=?", new String[] {messageUri.getLastPathSegment()}); - } - } - - @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("CellBroadcastHandler:"); - mLocalLog.dump(fd, pw, args); - pw.flush(); - } - - /** The callback interface of a location request. */ - public interface LocationUpdateCallback { - /** - * Call when the location update is available. - * @param location a location in (latitude, longitude) format, or {@code null} if the - * location service is not available. - */ - void onLocationUpdate(@Nullable LatLng location); - } - - private static final class LocationRequester { - private static final String TAG = LocationRequester.class.getSimpleName(); - - /** - * Use as the default maximum wait time if the cell broadcast doesn't specify the value. - * Most of the location request should be responded within 20 seconds. - */ - private static final int DEFAULT_MAXIMUM_WAIT_TIME_SEC = 20; - - /** - * Trigger this event when the {@link LocationManager} is not responded within the given - * time. - */ - private static final int EVENT_LOCATION_REQUEST_TIMEOUT = 1; - - /** Request a single location update. */ - private static final int EVENT_REQUEST_LOCATION_UPDATE = 2; - - /** - * Request location update from network or gps location provider. Network provider will be - * used if available, otherwise use the gps provider. - */ - private static final List<String> LOCATION_PROVIDERS = Arrays.asList( - LocationManager.NETWORK_PROVIDER, LocationManager.GPS_PROVIDER); - - private final LocationManager mLocationManager; - private final Looper mLooper; - private final List<LocationUpdateCallback> mCallbacks; - private final Context mContext; - private Handler mLocationHandler; - - LocationRequester(Context context, LocationManager locationManager, Looper looper) { - mLocationManager = locationManager; - mLooper = looper; - mCallbacks = new ArrayList<>(); - mContext = context; - mLocationHandler = new LocationHandler(looper); - } - - /** - * Request a single location update. If the location is not available, a callback with - * {@code null} location will be called immediately. - * - * @param callback a callback to the the response when the location is available - * @param maximumWaitTimeSec the maximum wait time of this request. If location is not - * updated within the maximum wait time, {@code callback#onLocationUpadte(null)} will be - * called. - */ - void requestLocationUpdate(@NonNull LocationUpdateCallback callback, - int maximumWaitTimeSec) { - mLocationHandler.obtainMessage(EVENT_REQUEST_LOCATION_UPDATE, maximumWaitTimeSec, - 0 /* arg2 */, callback).sendToTarget(); - } - - private void onLocationUpdate(@Nullable LatLng location) { - for (LocationUpdateCallback callback : mCallbacks) { - callback.onLocationUpdate(location); - } - mCallbacks.clear(); - } - - private void requestLocationUpdateInternal(@NonNull LocationUpdateCallback callback, - int maximumWaitTimeSec) { - if (DBG) Log.d(TAG, "requestLocationUpdate"); - if (!isLocationServiceAvailable()) { - if (DBG) { - Log.d(TAG, "Can't request location update because of no location permission"); - } - callback.onLocationUpdate(null); - return; - } - - if (!mLocationHandler.hasMessages(EVENT_LOCATION_REQUEST_TIMEOUT)) { - if (maximumWaitTimeSec == SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET) { - maximumWaitTimeSec = DEFAULT_MAXIMUM_WAIT_TIME_SEC; - } - mLocationHandler.sendMessageDelayed( - mLocationHandler.obtainMessage(EVENT_LOCATION_REQUEST_TIMEOUT), - maximumWaitTimeSec * DateUtils.SECOND_IN_MILLIS); - } - - mCallbacks.add(callback); - - for (String provider : LOCATION_PROVIDERS) { - if (mLocationManager.isProviderEnabled(provider)) { - mLocationManager.requestSingleUpdate(provider, mLocationListener, mLooper); - break; - } - } - } - - private boolean isLocationServiceAvailable() { - if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION) - && !hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) return false; - for (String provider : LOCATION_PROVIDERS) { - if (mLocationManager.isProviderEnabled(provider)) return true; - } - return false; - } - - private boolean hasPermission(String permission) { - return PermissionChecker.checkCallingOrSelfPermissionForDataDelivery(mContext, - permission) == PERMISSION_GRANTED; - } - - private final LocationListener mLocationListener = new LocationListener() { - @Override - public void onLocationChanged(Location location) { - mLocationHandler.removeMessages(EVENT_LOCATION_REQUEST_TIMEOUT); - onLocationUpdate(new LatLng(location.getLatitude(), location.getLongitude())); - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) {} - - @Override - public void onProviderEnabled(String provider) {} - - @Override - public void onProviderDisabled(String provider) {} - }; - - private final class LocationHandler extends Handler { - LocationHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case EVENT_LOCATION_REQUEST_TIMEOUT: - if (DBG) Log.d(TAG, "location request timeout"); - onLocationUpdate(null); - break; - case EVENT_REQUEST_LOCATION_UPDATE: - requestLocationUpdateInternal((LocationUpdateCallback) msg.obj, msg.arg1); - break; - default: - Log.e(TAG, "Unsupported message type " + msg.what); - } - } - } - } -} diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java b/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java index dde4042ad..a87d559c2 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java @@ -32,7 +32,6 @@ import android.content.Loader; import android.database.Cursor; import android.os.Bundle; import android.provider.Telephony; -import android.telephony.CellBroadcastMessage; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.LayoutInflater; diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastListItem.java b/src/com/android/cellbroadcastreceiver/CellBroadcastListItem.java index 56939b1a0..e4a8656e9 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastListItem.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastListItem.java @@ -18,7 +18,6 @@ package com.android.cellbroadcastreceiver; import android.content.Context; import android.graphics.Typeface; -import android.telephony.CellBroadcastMessage; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.style.StyleSpan; diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastMessage.java b/src/com/android/cellbroadcastreceiver/CellBroadcastMessage.java new file mode 100644 index 000000000..c20a61c91 --- /dev/null +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastMessage.java @@ -0,0 +1,462 @@ +/* + * Copyright (C) 2011 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.cellbroadcastreceiver; + +import android.annotation.NonNull; +import android.annotation.UnsupportedAppUsage; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.os.Parcel; +import android.os.Parcelable; +import android.provider.Telephony; +import android.provider.Telephony.CellBroadcasts; +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbEtwsInfo; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.telephony.SubscriptionManager; +import android.text.format.DateUtils; + +/** + * Application wrapper for {@link SmsCbMessage}. This is Parcelable so that + * decoded broadcast message objects can be passed between running Services. + * New broadcasts are received by the CellBroadcastReceiver app, which exports + * the database of previously received broadcasts at "content://cellbroadcasts/". + * The "android.permission.READ_CELL_BROADCASTS" permission is required to read + * from the ContentProvider, and writes to the database are not allowed.<p> + * + * Use {@link #createFromCursor} to create CellBroadcastMessage objects from rows + * in the database cursor returned by the ContentProvider. + * + * {@hide} + */ +public class CellBroadcastMessage implements Parcelable { + + /** Identifier for getExtra() when adding this object to an Intent. */ + public static final String SMS_CB_MESSAGE_EXTRA = + "com.android.cellbroadcastreceiver.SMS_CB_MESSAGE"; + + /** SmsCbMessage. */ + private final SmsCbMessage mSmsCbMessage; + + private final long mDeliveryTime; + private boolean mIsRead; + + /** + * get Subscription information + * + * @hide + */ + public int getSubId(@NonNull Context context) { + SubscriptionManager subMgr = (SubscriptionManager) context.getSystemService( + Context.TELEPHONY_SUBSCRIPTION_SERVICE); + int[] subs = subMgr.getSubscriptionIds(getSlotIndex()); + return (subs == null || subs.length == 0) ? SubscriptionManager.INVALID_SUBSCRIPTION_ID : + subs[0]; + } + + /** + * get slotIndex associated with this message. + * Note, cellbraocastMessage can be received without subscription. + * + * @hide + */ + public int getSlotIndex() { + return mSmsCbMessage.getSlotIndex(); + } + + @UnsupportedAppUsage + public CellBroadcastMessage(SmsCbMessage message) { + mSmsCbMessage = message; + mDeliveryTime = System.currentTimeMillis(); + mIsRead = false; + } + + private CellBroadcastMessage(SmsCbMessage message, long deliveryTime, boolean isRead) { + mSmsCbMessage = message; + mDeliveryTime = deliveryTime; + mIsRead = isRead; + } + + private CellBroadcastMessage(Parcel in) { + mSmsCbMessage = new SmsCbMessage(in); + mDeliveryTime = in.readLong(); + mIsRead = (in.readInt() != 0); + } + + /** Parcelable: no special flags. */ + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + mSmsCbMessage.writeToParcel(out, flags); + out.writeLong(mDeliveryTime); + out.writeInt(mIsRead ? 1 : 0); + } + + public static final Parcelable.Creator<CellBroadcastMessage> CREATOR + = new Parcelable.Creator<CellBroadcastMessage>() { + @Override + public CellBroadcastMessage createFromParcel(Parcel in) { + return new CellBroadcastMessage(in); + } + + @Override + public CellBroadcastMessage[] newArray(int size) { + return new CellBroadcastMessage[size]; + } + }; + + /** + * Create a CellBroadcastMessage from a row in the database. + * @param cursor an open SQLite cursor pointing to the row to read + * @return the new CellBroadcastMessage + * @throws IllegalArgumentException if one of the required columns is missing + */ + @UnsupportedAppUsage + public static CellBroadcastMessage createFromCursor(Cursor cursor) { + int geoScope = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE)); + int serialNum = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.SERIAL_NUMBER)); + int category = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.SERVICE_CATEGORY)); + String language = cursor.getString( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.LANGUAGE_CODE)); + String body = cursor.getString( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.MESSAGE_BODY)); + int format = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.MESSAGE_FORMAT)); + int priority = cursor.getInt( + cursor.getColumnIndexOrThrow(Telephony.CellBroadcasts.MESSAGE_PRIORITY)); + int slotIndex = cursor.getInt( + cursor.getColumnIndexOrThrow(CellBroadcasts.SLOT_INDEX)); + + String plmn; + int plmnColumn = cursor.getColumnIndex(Telephony.CellBroadcasts.PLMN); + if (plmnColumn != -1 && !cursor.isNull(plmnColumn)) { + plmn = cursor.getString(plmnColumn); + } else { + plmn = null; + } + + int lac; + int lacColumn = cursor.getColumnIndex(Telephony.CellBroadcasts.LAC); + if (lacColumn != -1 && !cursor.isNull(lacColumn)) { + lac = cursor.getInt(lacColumn); + } else { + lac = -1; + } + + int cid; + int cidColumn = cursor.getColumnIndex(Telephony.CellBroadcasts.CID); + if (cidColumn != -1 && !cursor.isNull(cidColumn)) { + cid = cursor.getInt(cidColumn); + } else { + cid = -1; + } + + SmsCbLocation location = new SmsCbLocation(plmn, lac, cid); + + SmsCbEtwsInfo etwsInfo; + int etwsWarningTypeColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.ETWS_WARNING_TYPE); + if (etwsWarningTypeColumn != -1 && !cursor.isNull(etwsWarningTypeColumn)) { + int warningType = cursor.getInt(etwsWarningTypeColumn); + etwsInfo = new SmsCbEtwsInfo(warningType, false, false, false, null); + } else { + etwsInfo = null; + } + + SmsCbCmasInfo cmasInfo; + int cmasMessageClassColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS); + if (cmasMessageClassColumn != -1 && !cursor.isNull(cmasMessageClassColumn)) { + int messageClass = cursor.getInt(cmasMessageClassColumn); + + int cmasCategory; + int cmasCategoryColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_CATEGORY); + if (cmasCategoryColumn != -1 && !cursor.isNull(cmasCategoryColumn)) { + cmasCategory = cursor.getInt(cmasCategoryColumn); + } else { + cmasCategory = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN; + } + + int responseType; + int cmasResponseTypeColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE); + if (cmasResponseTypeColumn != -1 && !cursor.isNull(cmasResponseTypeColumn)) { + responseType = cursor.getInt(cmasResponseTypeColumn); + } else { + responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN; + } + + int severity; + int cmasSeverityColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_SEVERITY); + if (cmasSeverityColumn != -1 && !cursor.isNull(cmasSeverityColumn)) { + severity = cursor.getInt(cmasSeverityColumn); + } else { + severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + } + + int urgency; + int cmasUrgencyColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_URGENCY); + if (cmasUrgencyColumn != -1 && !cursor.isNull(cmasUrgencyColumn)) { + urgency = cursor.getInt(cmasUrgencyColumn); + } else { + urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + } + + int certainty; + int cmasCertaintyColumn = cursor.getColumnIndex( + Telephony.CellBroadcasts.CMAS_CERTAINTY); + if (cmasCertaintyColumn != -1 && !cursor.isNull(cmasCertaintyColumn)) { + certainty = cursor.getInt(cmasCertaintyColumn); + } else { + certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + } + + cmasInfo = new SmsCbCmasInfo(messageClass, cmasCategory, responseType, severity, + urgency, certainty); + } else { + cmasInfo = null; + } + + SmsCbMessage msg = new SmsCbMessage(format, geoScope, serialNum, location, category, + language, body, priority, etwsInfo, cmasInfo, slotIndex); + + long deliveryTime = cursor.getLong(cursor.getColumnIndexOrThrow( + Telephony.CellBroadcasts.DELIVERY_TIME)); + boolean isRead = (cursor.getInt(cursor.getColumnIndexOrThrow( + Telephony.CellBroadcasts.MESSAGE_READ)) != 0); + + return new CellBroadcastMessage(msg, deliveryTime, isRead); + } + + /** + * Return a ContentValues object for insertion into the database. + * @return a new ContentValues object containing this object's data + */ + @UnsupportedAppUsage + public ContentValues getContentValues() { + ContentValues cv = new ContentValues(16); + SmsCbMessage msg = mSmsCbMessage; + cv.put(CellBroadcasts.SLOT_INDEX, msg.getSlotIndex()); + cv.put(Telephony.CellBroadcasts.GEOGRAPHICAL_SCOPE, msg.getGeographicalScope()); + SmsCbLocation location = msg.getLocation(); + if (location.getPlmn() != null) { + cv.put(Telephony.CellBroadcasts.PLMN, location.getPlmn()); + } + if (location.getLac() != -1) { + cv.put(Telephony.CellBroadcasts.LAC, location.getLac()); + } + if (location.getCid() != -1) { + cv.put(Telephony.CellBroadcasts.CID, location.getCid()); + } + cv.put(Telephony.CellBroadcasts.SERIAL_NUMBER, msg.getSerialNumber()); + cv.put(Telephony.CellBroadcasts.SERVICE_CATEGORY, msg.getServiceCategory()); + cv.put(Telephony.CellBroadcasts.LANGUAGE_CODE, msg.getLanguageCode()); + cv.put(Telephony.CellBroadcasts.MESSAGE_BODY, msg.getMessageBody()); + cv.put(Telephony.CellBroadcasts.DELIVERY_TIME, mDeliveryTime); + cv.put(Telephony.CellBroadcasts.MESSAGE_READ, mIsRead); + cv.put(Telephony.CellBroadcasts.MESSAGE_FORMAT, msg.getMessageFormat()); + cv.put(Telephony.CellBroadcasts.MESSAGE_PRIORITY, msg.getMessagePriority()); + + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + if (etwsInfo != null) { + cv.put(Telephony.CellBroadcasts.ETWS_WARNING_TYPE, etwsInfo.getWarningType()); + } + + SmsCbCmasInfo cmasInfo = mSmsCbMessage.getCmasWarningInfo(); + if (cmasInfo != null) { + cv.put(Telephony.CellBroadcasts.CMAS_MESSAGE_CLASS, cmasInfo.getMessageClass()); + cv.put(Telephony.CellBroadcasts.CMAS_CATEGORY, cmasInfo.getCategory()); + cv.put(Telephony.CellBroadcasts.CMAS_RESPONSE_TYPE, cmasInfo.getResponseType()); + cv.put(Telephony.CellBroadcasts.CMAS_SEVERITY, cmasInfo.getSeverity()); + cv.put(Telephony.CellBroadcasts.CMAS_URGENCY, cmasInfo.getUrgency()); + cv.put(Telephony.CellBroadcasts.CMAS_CERTAINTY, cmasInfo.getCertainty()); + } + + return cv; + } + + /** + * Set or clear the "read message" flag. + * @param isRead true if the message has been read; false if not + */ + public void setIsRead(boolean isRead) { + mIsRead = isRead; + } + + @UnsupportedAppUsage + public String getLanguageCode() { + return mSmsCbMessage.getLanguageCode(); + } + + @UnsupportedAppUsage + public int getServiceCategory() { + return mSmsCbMessage.getServiceCategory(); + } + + @UnsupportedAppUsage + public long getDeliveryTime() { + return mDeliveryTime; + } + + @UnsupportedAppUsage + public String getMessageBody() { + return mSmsCbMessage.getMessageBody(); + } + + @UnsupportedAppUsage + public boolean isRead() { + return mIsRead; + } + + @UnsupportedAppUsage + public int getSerialNumber() { + return mSmsCbMessage.getSerialNumber(); + } + + public SmsCbCmasInfo getCmasWarningInfo() { + return mSmsCbMessage.getCmasWarningInfo(); + } + + @UnsupportedAppUsage + public SmsCbEtwsInfo getEtwsWarningInfo() { + return mSmsCbMessage.getEtwsWarningInfo(); + } + + /** + * Return whether the broadcast is an emergency (PWS) message type. + * This includes lower priority test messages and Amber alerts. + * + * All public alerts show the flashing warning icon in the dialog, + * but only emergency alerts play the alert sound and speak the message. + * + * @return true if the message is PWS type; false otherwise + */ + public boolean isPublicAlertMessage() { + return mSmsCbMessage.isEmergencyMessage(); + } + + /** + * Returns whether the broadcast is an emergency (PWS) message type, + * including test messages and AMBER alerts. + * + * @return true if the message is PWS type (ETWS or CMAS) + */ + @UnsupportedAppUsage + public boolean isEmergencyAlertMessage() { + return mSmsCbMessage.isEmergencyMessage(); + } + + /** + * Return whether the broadcast is an ETWS emergency message type. + * @return true if the message is ETWS emergency type; false otherwise + */ + @UnsupportedAppUsage + public boolean isEtwsMessage() { + return mSmsCbMessage.isEtwsMessage(); + } + + /** + * Return whether the broadcast is a CMAS emergency message type. + * @return true if the message is CMAS emergency type; false otherwise + */ + @UnsupportedAppUsage + public boolean isCmasMessage() { + return mSmsCbMessage.isCmasMessage(); + } + + /** + * Return the CMAS message class. + * @return the CMAS message class, e.g. {@link SmsCbCmasInfo#CMAS_CLASS_SEVERE_THREAT}, or + * {@link SmsCbCmasInfo#CMAS_CLASS_UNKNOWN} if this is not a CMAS alert + */ + public int getCmasMessageClass() { + if (mSmsCbMessage.isCmasMessage()) { + return mSmsCbMessage.getCmasWarningInfo().getMessageClass(); + } else { + return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; + } + } + + /** + * Return whether the broadcast is an ETWS popup alert. + * This method checks the message ID and the message code. + * @return true if the message indicates an ETWS popup alert + */ + public boolean isEtwsPopupAlert() { + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + return etwsInfo != null && etwsInfo.isPopupAlert(); + } + + /** + * Return whether the broadcast is an ETWS emergency user alert. + * This method checks the message ID and the message code. + * @return true if the message indicates an ETWS emergency user alert + */ + public boolean isEtwsEmergencyUserAlert() { + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + return etwsInfo != null && etwsInfo.isEmergencyUserAlert(); + } + + /** + * Return whether the broadcast is an ETWS test message. + * @return true if the message is an ETWS test message; false otherwise + */ + public boolean isEtwsTestMessage() { + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + return etwsInfo != null && + etwsInfo.getWarningType() == SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE; + } + + /** + * Return the abbreviated date string for the message delivery time. + * @param context the context object + * @return a String to use in the broadcast list UI + */ + public String getDateString(Context context) { + int flags = DateUtils.FORMAT_NO_NOON_MIDNIGHT | DateUtils.FORMAT_SHOW_TIME | + DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_SHOW_DATE | + DateUtils.FORMAT_CAP_AMPM; + return DateUtils.formatDateTime(context, mDeliveryTime, flags); + } + + /** + * Return the date string for the message delivery time, suitable for text-to-speech. + * @param context the context object + * @return a String for populating the list item AccessibilityEvent for TTS + */ + @UnsupportedAppUsage + public String getSpokenDateString(Context context) { + int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_DATE; + return DateUtils.formatDateTime(context, mDeliveryTime, flags); + } + + public SmsCbMessage getSmsCbMessage() { + return mSmsCbMessage; + } +} diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiver.java b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiver.java index c94b96bfa..670f70905 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiver.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiver.java @@ -108,8 +108,9 @@ public class CellBroadcastReceiver extends BroadcastReceiver { // rename registered notification channels on locale change CellBroadcastAlertService.createNotificationChannels(context); } else if (Intent.ACTION_SERVICE_STATE.equals(action)) { - if (CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context).getBoolean( - R.bool.reset_duplicate_detection_on_airplane_mode)) { + if (CellBroadcastSettings.getResources(context, + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID).getBoolean( + R.bool.reset_duplicate_detection_on_airplane_mode)) { Bundle extras = intent.getExtras(); ServiceState ss = extras.getParcelable(Intent.EXTRA_SERVICE_STATE); if (ss.getState() == ServiceState.STATE_POWER_OFF) { @@ -126,9 +127,9 @@ public class CellBroadcastReceiver extends BroadcastReceiver { String currentIntervalDefault = sp.getString(CURRENT_INTERVAL_DEFAULT, "0"); // If interval default changes, reset the interval to the new default value. - String newIntervalDefault = - CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context) - .getString(R.string.alert_reminder_interval_default_value); + String newIntervalDefault = CellBroadcastSettings.getResources(context, + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID).getString( + R.string.alert_reminder_interval_default_value); if (!newIntervalDefault.equals(currentIntervalDefault)) { Log.d(TAG, "Default interval changed from " + currentIntervalDefault + " to " + newIntervalDefault); diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java index 44c1f8427..811a1924f 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java @@ -17,7 +17,6 @@ package com.android.cellbroadcastreceiver; import android.app.Application; -import android.telephony.CellBroadcastMessage; import java.util.ArrayList; diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastResources.java b/src/com/android/cellbroadcastreceiver/CellBroadcastResources.java index 25952cedb..cf5420a0d 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastResources.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastResources.java @@ -18,7 +18,6 @@ package com.android.cellbroadcastreceiver; import android.content.Context; import android.graphics.Typeface; -import android.telephony.CellBroadcastMessage; import android.telephony.SmsCbCmasInfo; import android.telephony.SmsCbEtwsInfo; import android.text.Spannable; @@ -264,22 +263,21 @@ public class CellBroadcastResources { } SmsCbCmasInfo cmasInfo = cbm.getCmasWarningInfo(); - int subId = cbm.getSubId(); + int subId = cbm.getSubId(context); + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + context, subId); final int serviceCategory = cbm.getServiceCategory(); - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, - R.array.emergency_alerts_channels_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.emergency_alerts_channels_range_strings)) { return R.string.pws_other_message_identifiers; } // CMAS warning types - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, - R.array.cmas_presidential_alerts_channels_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.cmas_presidential_alerts_channels_range_strings)) { return R.string.cmas_presidential_level_alert; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, - R.array.cmas_alert_extreme_channels_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.cmas_alert_extreme_channels_range_strings)) { if (cmasInfo.getSeverity() == SmsCbCmasInfo.CMAS_SEVERITY_EXTREME && cmasInfo.getUrgency() == SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE) { if (cmasInfo.getCertainty() == SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED) { @@ -290,40 +288,39 @@ public class CellBroadcastResources { } return R.string.cmas_extreme_alert; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, R.array.cmas_alerts_severe_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.cmas_alerts_severe_range_strings)) { return R.string.cmas_severe_alert; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, - R.array.cmas_amber_alerts_channels_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.cmas_amber_alerts_channels_range_strings)) { return R.string.cmas_amber_alert; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, R.array.required_monthly_test_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.required_monthly_test_range_strings)) { return R.string.cmas_required_monthly_test; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, R.array.exercise_alert_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.exercise_alert_range_strings)) { return R.string.cmas_exercise_alert; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, R.array.operator_defined_alert_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.operator_defined_alert_range_strings)) { return R.string.cmas_operator_defined_alert; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, R.array.public_safety_messages_channels_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.public_safety_messages_channels_range_strings)) { return R.string.public_safety_message; } - if (CellBroadcastChannelManager.checkCellBroadcastChannelRange(subId, - serviceCategory, R.array.state_local_test_alert_range_strings, context)) { + if (channelManager.checkCellBroadcastChannelRange(serviceCategory, + R.array.state_local_test_alert_range_strings)) { return R.string.state_local_test_alert; } - if (CellBroadcastChannelManager.isEmergencyMessage(context, cbm)) { + if (channelManager.isEmergencyMessage(cbm)) { ArrayList<CellBroadcastChannelRange> ranges = - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - context, R.array.additional_cbs_channels_strings); + channelManager.getCellBroadcastChannelRanges( + R.array.additional_cbs_channels_strings); if (ranges != null) { for (CellBroadcastChannelRange range : ranges) { if (serviceCategory >= range.mStartId && serviceCategory <= range.mEndId) { diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastSearchIndexableProvider.java b/src/com/android/cellbroadcastreceiver/CellBroadcastSearchIndexableProvider.java index 8a3059c92..2134e4631 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastSearchIndexableProvider.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastSearchIndexableProvider.java @@ -41,6 +41,7 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.provider.SearchIndexableResource; import android.provider.SearchIndexablesProvider; +import android.telephony.SubscriptionManager; import android.text.TextUtils; import java.util.ArrayList; @@ -88,8 +89,8 @@ public class CellBroadcastSearchIndexableProvider extends SearchIndexablesProvid @Override public Cursor queryRawData(String[] projection) { MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS); - final Resources res = - CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(getContext()); + final Resources res = CellBroadcastSettings.getResources(getContext(), + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); Object[] raw = new Object[INDEXABLES_RAW_COLUMNS.length]; raw[COLUMN_INDEX_RAW_TITLE] = res.getString(R.string.sms_cb_settings); @@ -98,14 +99,15 @@ public class CellBroadcastSearchIndexableProvider extends SearchIndexablesProvid keywordList.add(res.getString(keywordRes)); } - if (!CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(getContext(), + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); + + if (!channelManager.getCellBroadcastChannelRanges( R.array.public_safety_messages_channels_range_strings).isEmpty()) { keywordList.add(res.getString(R.string.public_safety_message)); } - if (!CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), + if (!channelManager.getCellBroadcastChannelRanges( R.array.state_local_test_alert_range_strings).isEmpty()) { keywordList.add(res.getString(R.string.state_local_test_alert)); } @@ -130,7 +132,8 @@ public class CellBroadcastSearchIndexableProvider extends SearchIndexablesProvid boolean enableDevSettings = DevelopmentSettingsHelper.isDevelopmentSettingsEnabled(getContext()); - Resources res = CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(getContext()); + Resources res = CellBroadcastSettings.getResources(getContext(), + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); Object[] ref; ref = new Object[1]; @@ -178,6 +181,13 @@ public class CellBroadcastSearchIndexableProvider extends SearchIndexablesProvid cursor.addRow(ref); } + if (!Resources.getSystem().getBoolean(R.bool.show_presidential_alerts_in_settings)) { + ref = new Object[1]; + ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = + CellBroadcastSettings.KEY_ENABLE_CMAS_PRESIDENTIAL_ALERTS; + cursor.addRow(ref); + } + if (!enableDevSettings) { ref = new Object[1]; ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = @@ -185,6 +195,40 @@ public class CellBroadcastSearchIndexableProvider extends SearchIndexablesProvid cursor.addRow(ref); } + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(getContext(), + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); + if (channelManager.getCellBroadcastChannelRanges( + R.array.cmas_amber_alerts_channels_range_strings).isEmpty()) { + ref = new Object[1]; + ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = + CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS; + cursor.addRow(ref); + } + + if (channelManager.getCellBroadcastChannelRanges( + R.array.emergency_alerts_channels_range_strings).isEmpty()) { + ref = new Object[1]; + ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = + CellBroadcastSettings.KEY_ENABLE_EMERGENCY_ALERTS; + cursor.addRow(ref); + } + + if (channelManager.getCellBroadcastChannelRanges( + R.array.public_safety_messages_channels_range_strings).isEmpty()) { + ref = new Object[1]; + ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = + CellBroadcastSettings.KEY_ENABLE_PUBLIC_SAFETY_MESSAGES; + cursor.addRow(ref); + } + + if (channelManager.getCellBroadcastChannelRanges( + R.array.state_local_test_alert_range_strings).isEmpty()) { + ref = new Object[1]; + ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = + CellBroadcastSettings.KEY_ENABLE_STATE_LOCAL_TEST_ALERTS; + cursor.addRow(ref); + } + return cursor; } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java b/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java index 3f1331e28..67f23f6db 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java @@ -16,6 +16,7 @@ package com.android.cellbroadcastreceiver; +import android.annotation.NonNull; import android.app.ActionBar; import android.app.Activity; import android.app.Fragment; @@ -80,6 +81,11 @@ public class CellBroadcastSettings extends Activity { // Preference category for alert preferences. public static final String KEY_CATEGORY_ALERT_PREFERENCES = "category_alert_preferences"; + // Show checkbox for Presidential alerts in settings + // Whether to display CMAS presidential alert notifications (always enabled). + public static final String KEY_ENABLE_CMAS_PRESIDENTIAL_ALERTS = + "enable_cmas_presidential_alerts"; + // Whether to display CMAS extreme threat notifications (default is enabled). public static final String KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS = "enable_cmas_extreme_threat_alerts"; @@ -182,6 +188,9 @@ public class CellBroadcastSettings extends Activity { // WATCH private TwoStatePreference mAlertReminder; + // Show checkbox for Presidential alerts in settings + private TwoStatePreference mPresidentialCheckBox; + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -223,6 +232,10 @@ public class CellBroadcastSettings extends Activity { mDevSettingCategory = (PreferenceCategory) findPreference(KEY_CATEGORY_DEV_SETTINGS); + // Show checkbox for Presidential alerts in settings + mPresidentialCheckBox = (TwoStatePreference) + findPreference(KEY_ENABLE_CMAS_PRESIDENTIAL_ALERTS); + if (pm.hasSystemFeature(PackageManager.FEATURE_WATCH)) { mAlertReminder = (TwoStatePreference) findPreference(KEY_WATCH_ALERT_REMINDER); @@ -286,7 +299,8 @@ public class CellBroadcastSettings extends Activity { boolean enableDevSettings = DevelopmentSettingsHelper.isDevelopmentSettingsEnabled(getContext()); - Resources res = getResourcesForDefaultSmsSubscriptionId(getContext()); + Resources res = CellBroadcastSettings.getResources(getContext(), + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); initReminderIntervalList(); boolean emergencyAlertOnOffOptionEnabled = isFeatureEnabled(getContext(), @@ -309,8 +323,20 @@ public class CellBroadcastSettings extends Activity { boolean hideTestAlertMenu = CellBroadcastSettings.isFeatureEnabled(getContext(), CarrierConfigManager.KEY_CARRIER_FORCE_DISABLE_ETWS_CMAS_TEST_BOOL, false); + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager( + getContext(), SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); + + boolean isTestAlertsAvailable = !channelManager.getCellBroadcastChannelRanges( + R.array.required_monthly_test_range_strings).isEmpty() + || !channelManager.getCellBroadcastChannelRanges( + R.array.exercise_alert_range_strings).isEmpty() + || !channelManager.getCellBroadcastChannelRanges( + R.array.operator_defined_alert_range_strings).isEmpty() + || !channelManager.getCellBroadcastChannelRanges( + R.array.etws_test_alerts_range_strings).isEmpty(); + // Check if we want to hide the test alert toggle. - if (hideTestAlertMenu || !enableDevSettings || !isTestAlertsAvailable()) { + if (hideTestAlertMenu || !enableDevSettings || !isTestAlertsAvailable) { if (mTestCheckBox != null) { mAlertCategory.removePreference(mTestCheckBox); } @@ -342,9 +368,20 @@ public class CellBroadcastSettings extends Activity { } // Remove preferences based on range configurations - if (CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), - R.array.public_safety_messages_channels_range_strings).isEmpty()) { + if (channelManager.getCellBroadcastChannelRanges( + R.array.cmas_amber_alerts_channels_range_strings).isEmpty()) { + // Remove ambert alert + if (mAlertCategory != null) { + if (mAmberCheckBox != null) { + mAlertCategory.removePreference(mAmberCheckBox); + } + } + } + + // Remove preferences based on range configurations + if (channelManager.getCellBroadcastChannelRanges( + R.array.public_safety_messages_channels_range_strings).isEmpty() || + !res.getBoolean(R.bool.show_public_safety_settings)) { // Remove public safety messages if (mAlertCategory != null) { if (mPublicSafetyMessagesChannelCheckBox != null) { @@ -353,8 +390,8 @@ public class CellBroadcastSettings extends Activity { } } - if (CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), R.array.emergency_alerts_channels_range_strings).isEmpty()) { + if (channelManager.getCellBroadcastChannelRanges( + R.array.emergency_alerts_channels_range_strings).isEmpty()) { // Remove emergency alert messages if (mAlertCategory != null) { if (mEmergencyAlertsCheckBox != null) { @@ -363,8 +400,7 @@ public class CellBroadcastSettings extends Activity { } } - if (CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), + if (channelManager.getCellBroadcastChannelRanges( R.array.state_local_test_alert_range_strings).isEmpty()) { // Remove state local test messages if (mAlertCategory != null) { @@ -431,22 +467,18 @@ public class CellBroadcastSettings extends Activity { } }); } - } - private boolean isTestAlertsAvailable() { - return !CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), R.array.required_monthly_test_range_strings).isEmpty() - || !CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), R.array.exercise_alert_range_strings).isEmpty() - || !CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), R.array.operator_defined_alert_range_strings) - .isEmpty() - || !CellBroadcastChannelManager.getCellBroadcastChannelRanges( - this.getContext(), R.array.etws_test_alerts_range_strings).isEmpty(); + // Show checkbox for Presidential alerts in settings + if (!res.getBoolean(R.bool.show_presidential_alerts_in_settings)) { + if (mAlertCategory != null) { + mAlertCategory.removePreference(mPresidentialCheckBox); + } + } } private void initReminderIntervalList() { - Resources res = getResourcesForDefaultSmsSubscriptionId(getContext()); + Resources res = CellBroadcastSettings.getResources( + getContext(), SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); String[] activeValues = res.getStringArray(R.array.alert_reminder_interval_active_values); @@ -519,20 +551,11 @@ public class CellBroadcastSettings extends Activity { } public static boolean isFeatureEnabled(Context context, String feature, boolean defaultValue) { - int subId = SubscriptionManager.getDefaultSmsSubscriptionId(); - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - subId = SubscriptionManager.getDefaultSubscriptionId(); - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - return defaultValue; - } - } - CarrierConfigManager configManager = (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE); if (configManager != null) { - PersistableBundle carrierConfig = configManager.getConfigForSubId(subId); - + PersistableBundle carrierConfig = configManager.getConfig(); if (carrierConfig != null) { return carrierConfig.getBoolean(feature, defaultValue); } @@ -541,13 +564,18 @@ public class CellBroadcastSettings extends Activity { return defaultValue; } - public static Resources getResourcesForDefaultSmsSubscriptionId(Context context) { - int subId = SubscriptionManager.getDefaultSmsSubscriptionId(); - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - subId = SubscriptionManager.getDefaultSubscriptionId(); - if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - return context.getResources(); - } + /** + * Get the device resource based on SIM + * + * @param context Context + * @param subId Subscription index + * + * @return The resource + */ + public static @NonNull Resources getResources(@NonNull Context context, int subId) { + if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID + || !SubscriptionManager.isValidSubscriptionId(subId)) { + return context.getResources(); } if (sResourcesCache.containsKey(subId)) { diff --git a/src/com/android/cellbroadcastreceiver/DefaultCellBroadcastService.java b/src/com/android/cellbroadcastreceiver/DefaultCellBroadcastService.java deleted file mode 100644 index a72bb188b..000000000 --- a/src/com/android/cellbroadcastreceiver/DefaultCellBroadcastService.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.cellbroadcastreceiver; - -import android.telephony.CellBroadcastService; -import android.util.Log; - -/** - * The default implementation of CellBroadcastService, which is used for handling GSM and CDMA - * cell broadcast messages. - */ -public class DefaultCellBroadcastService extends CellBroadcastService { - private GsmCellBroadcastHandler mGsmCellBroadcastHandler; - - private static final String TAG = "DefaultCellBroadcastService"; - - @Override - public void onCreate() { - super.onCreate(); - mGsmCellBroadcastHandler = - GsmCellBroadcastHandler.makeGsmCellBroadcastHandler(getApplicationContext()); - } - - @Override - public void onGsmCellBroadcastSms(int slotIndex, byte[] message) { - Log.d(TAG, "onGsmCellBroadcastSms received message on slotId=" + slotIndex); - mGsmCellBroadcastHandler.onGsmCellBroadcastSms(slotIndex, message); - } - - @Override - public void onCdmaCellBroadcastSms(int slotIndex, byte[] message) { - // TODO implement CDMA - } -} diff --git a/src/com/android/cellbroadcastreceiver/GsmCellBroadcastHandler.java b/src/com/android/cellbroadcastreceiver/GsmCellBroadcastHandler.java deleted file mode 100644 index 6a7d3469d..000000000 --- a/src/com/android/cellbroadcastreceiver/GsmCellBroadcastHandler.java +++ /dev/null @@ -1,404 +0,0 @@ -/* - * Copyright (C) 2013 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.cellbroadcastreceiver; - -import static com.android.internal.telephony.gsm.SmsCbConstants.MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER; - -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Message; -import android.provider.Telephony.CellBroadcasts; -import android.telephony.CellInfo; -import android.telephony.CellLocation; -import android.telephony.SmsCbLocation; -import android.telephony.SmsCbMessage; -import android.telephony.TelephonyManager; -import android.telephony.gsm.GsmCellLocation; -import android.text.format.DateUtils; - -import com.android.cellbroadcastreceiver.GsmSmsCbMessage.GeoFencingTriggerMessage; -import com.android.cellbroadcastreceiver.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity; -import com.android.internal.telephony.CbGeoUtils.Geometry; - -import dalvik.annotation.compat.UnsupportedAppUsage; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; - -/** - * Handler for 3GPP format Cell Broadcasts. Parent class can also handle CDMA Cell Broadcasts. - */ -public class GsmCellBroadcastHandler extends CellBroadcastHandler { - private static final boolean VDBG = false; // log CB PDU data - - /** Indicates that a message is not being broadcasted. */ - private static final String MESSAGE_NOT_BROADCASTED = "0"; - - /** This map holds incomplete concatenated messages waiting for assembly. */ - @UnsupportedAppUsage - private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap = - new HashMap<>(4); - - protected GsmCellBroadcastHandler(Context context) { - super("GsmCellBroadcastHandler", context); - } - - @Override - protected void onQuitting() { - super.onQuitting(); // release wakelock - } - - /** - * Handle a GSM cell broadcast message passed from the telephony framework. - * @param message - */ - public void onGsmCellBroadcastSms(int slotIndex, byte[] message) { - sendMessage(EVENT_NEW_SMS_MESSAGE, slotIndex, -1, message); - } - - /** - * Create a new CellBroadcastHandler. - * @param context the context to use for dispatching Intents - * @return the new handler - */ - public static GsmCellBroadcastHandler makeGsmCellBroadcastHandler(Context context) { - GsmCellBroadcastHandler handler = new GsmCellBroadcastHandler(context); - handler.start(); - return handler; - } - - /** - * Find the cell broadcast messages specify by the geo-fencing trigger message and perform a - * geo-fencing check for these messages. - * @param geoFencingTriggerMessage the trigger message - * - * @return {@code True} if geo-fencing is need for some cell broadcast message. - */ - private boolean handleGeoFencingTriggerMessage( - GeoFencingTriggerMessage geoFencingTriggerMessage, int slotIndex) { - final List<SmsCbMessage> cbMessages = new ArrayList<>(); - final List<Uri> cbMessageUris = new ArrayList<>(); - - // Only consider the cell broadcast received within 24 hours. - long lastReceivedTime = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS; - - // Find the cell broadcast message identify by the message identifier and serial number - // and is not broadcasted. - String where = CellBroadcasts.SERVICE_CATEGORY + "=? AND " - + CellBroadcasts.SERIAL_NUMBER + "=? AND " - + CellBroadcasts.MESSAGE_BROADCASTED + "=? AND " - + CellBroadcasts.RECEIVED_TIME + ">?"; - - ContentResolver resolver = mContext.getContentResolver(); - for (CellBroadcastIdentity identity : geoFencingTriggerMessage.cbIdentifiers) { - try (Cursor cursor = resolver.query(CELL_BROADCAST_URI, - CellBroadcasts.QUERY_COLUMNS_FWK, - where, - new String[] { Integer.toString(identity.messageIdentifier), - Integer.toString(identity.serialNumber), MESSAGE_NOT_BROADCASTED, - Long.toString(lastReceivedTime) }, - null /* sortOrder */)) { - if (cursor != null) { - while (cursor.moveToNext()) { - cbMessages.add(SmsCbMessage.createFromCursor(cursor)); - cbMessageUris.add(ContentUris.withAppendedId(CELL_BROADCAST_URI, - cursor.getInt(cursor.getColumnIndex(CellBroadcasts._ID)))); - } - } - } - } - - List<Geometry> commonBroadcastArea = new ArrayList<>(); - if (geoFencingTriggerMessage.shouldShareBroadcastArea()) { - for (SmsCbMessage msg : cbMessages) { - if (msg.getGeometries() != null) { - commonBroadcastArea.addAll(msg.getGeometries()); - } - } - } - - // ATIS doesn't specify the geo fencing maximum wait time for the cell broadcasts specified - // in geo fencing trigger message. We will pick the largest maximum wait time among these - // cell broadcasts. - int maximumWaitTimeSec = 0; - for (SmsCbMessage msg : cbMessages) { - maximumWaitTimeSec = Math.max(maximumWaitTimeSec, msg.getMaximumWaitingTime()); - } - - if (DBG) { - logd("Geo-fencing trigger message = " + geoFencingTriggerMessage); - for (SmsCbMessage msg : cbMessages) { - logd(msg.toString()); - } - } - - if (cbMessages.isEmpty()) { - if (DBG) logd("No CellBroadcast message need to be broadcasted"); - return false; - } - - requestLocationUpdate(location -> { - if (location == null) { - // If the location is not available, broadcast the messages directly. - broadcastMessage(cbMessages, cbMessageUris, slotIndex); - } else { - for (int i = 0; i < cbMessages.size(); i++) { - List<Geometry> broadcastArea = !commonBroadcastArea.isEmpty() - ? commonBroadcastArea : cbMessages.get(i).getGeometries(); - if (broadcastArea == null || broadcastArea.isEmpty()) { - broadcastMessage(cbMessages.get(i), cbMessageUris.get(i), slotIndex); - } else { - performGeoFencing(cbMessages.get(i), cbMessageUris.get(i), broadcastArea, - location, slotIndex); - } - } - } - }, maximumWaitTimeSec); - return true; - } - - /** - * Handle 3GPP-format Cell Broadcast messages sent from radio. - * - * @param message the message to process - * @return true if need to wait for geo-fencing or an ordered broadcast was sent. - */ - @Override - protected boolean handleSmsMessage(Message message) { - // For GSM, message.obj should be a byte[] - int slotIndex = message.arg1; - if (message.obj instanceof byte[]) { - byte[] pdu = (byte[]) message.obj; - SmsCbHeader header = createSmsCbHeader(pdu); - if (header == null) return false; - - if (header.getServiceCategory() == MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER) { - GeoFencingTriggerMessage triggerMessage = - GsmSmsCbMessage.createGeoFencingTriggerMessage(pdu); - if (triggerMessage != null) { - return handleGeoFencingTriggerMessage(triggerMessage, slotIndex); - } - } else { - SmsCbMessage cbMessage = handleGsmBroadcastSms(header, pdu, slotIndex); - if (cbMessage != null) { - handleBroadcastSms(cbMessage, slotIndex); - return true; - } - if (VDBG) log("Not handled GSM broadcasts."); - } - } - return super.handleSmsMessage(message); - } - - // return the cell location from the first returned cell info, prioritizing GSM - private CellLocation getCellLocation() { - TelephonyManager tm = - (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); - List<CellInfo> infos = tm.getAllCellInfo(); - for (CellInfo info : infos) { - CellLocation cl = info.getCellIdentity().asCellLocation(); - if (cl instanceof GsmCellLocation) { - return cl; - } - } - // If no GSM, return first in list - if (infos != null && !infos.isEmpty() && infos.get(0) != null) { - return infos.get(0).getCellIdentity().asCellLocation(); - } - return CellLocation.getEmpty(); - } - - - /** - * Handle 3GPP format SMS-CB message. - * @param header the cellbroadcast header. - * @param receivedPdu the received PDUs as a byte[] - */ - private SmsCbMessage handleGsmBroadcastSms(SmsCbHeader header, byte[] receivedPdu, - int slotIndex) { - try { - if (VDBG) { - int pduLength = receivedPdu.length; - for (int i = 0; i < pduLength; i += 8) { - StringBuilder sb = new StringBuilder("SMS CB pdu data: "); - for (int j = i; j < i + 8 && j < pduLength; j++) { - int b = receivedPdu[j] & 0xff; - if (b < 0x10) { - sb.append('0'); - } - sb.append(Integer.toHexString(b)).append(' '); - } - log(sb.toString()); - } - } - - if (VDBG) log("header=" + header); - TelephonyManager tm = - (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); - // TODO make a systemAPI for getNetworkOperatorForSlotIndex - String plmn = tm.getNetworkOperatorForPhone(slotIndex); - int lac = -1; - int cid = -1; - CellLocation cl = getCellLocation(); - // Check if cell location is GsmCellLocation. This is required to support - // dual-mode devices such as CDMA/LTE devices that require support for - // both 3GPP and 3GPP2 format messages - if (cl instanceof GsmCellLocation) { - GsmCellLocation cellLocation = (GsmCellLocation) cl; - lac = cellLocation.getLac(); - cid = cellLocation.getCid(); - } - - SmsCbLocation location; - switch (header.getGeographicalScope()) { - case SmsCbMessage.GEOGRAPHICAL_SCOPE_LOCATION_AREA_WIDE: - location = new SmsCbLocation(plmn, lac, -1); - break; - - case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE: - case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: - location = new SmsCbLocation(plmn, lac, cid); - break; - - case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE: - default: - location = new SmsCbLocation(plmn); - break; - } - - byte[][] pdus; - int pageCount = header.getNumberOfPages(); - if (pageCount > 1) { - // Multi-page message - SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location); - - // Try to find other pages of the same message - pdus = mSmsCbPageMap.get(concatInfo); - - if (pdus == null) { - // This is the first page of this message, make room for all - // pages and keep until complete - pdus = new byte[pageCount][]; - - mSmsCbPageMap.put(concatInfo, pdus); - } - - if (VDBG) log("pdus size=" + pdus.length); - // Page parameter is one-based - pdus[header.getPageIndex() - 1] = receivedPdu; - - for (byte[] pdu : pdus) { - if (pdu == null) { - // Still missing pages, exit - log("still missing pdu"); - return null; - } - } - - // Message complete, remove and dispatch - mSmsCbPageMap.remove(concatInfo); - } else { - // Single page message - pdus = new byte[1][]; - pdus[0] = receivedPdu; - } - - // Remove messages that are out of scope to prevent the map from - // growing indefinitely, containing incomplete messages that were - // never assembled - Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator(); - - while (iter.hasNext()) { - SmsCbConcatInfo info = iter.next(); - - if (!info.matchesLocation(plmn, lac, cid)) { - iter.remove(); - } - } - - return GsmSmsCbMessage.createSmsCbMessage(mContext, header, location, pdus); - - } catch (RuntimeException e) { - loge("Error in decoding SMS CB pdu", e); - return null; - } - } - - private SmsCbHeader createSmsCbHeader(byte[] bytes) { - try { - return new SmsCbHeader(bytes); - } catch (Exception ex) { - loge("Can't create SmsCbHeader, ex = " + ex.toString()); - return null; - } - } - - /** - * Holds all info about a message page needed to assemble a complete concatenated message. - */ - private static final class SmsCbConcatInfo { - - private final SmsCbHeader mHeader; - private final SmsCbLocation mLocation; - - @UnsupportedAppUsage - SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) { - mHeader = header; - mLocation = location; - } - - @Override - public int hashCode() { - return (mHeader.getSerialNumber() * 31) + mLocation.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof SmsCbConcatInfo) { - SmsCbConcatInfo other = (SmsCbConcatInfo) obj; - - // Two pages match if they have the same serial number (which includes the - // geographical scope and update number), and both pages belong to the same - // location (PLMN, plus LAC and CID if these are part of the geographical scope). - return mHeader.getSerialNumber() == other.mHeader.getSerialNumber() - && mLocation.equals(other.mLocation); - } - - return false; - } - - /** - * Compare the location code for this message to the current location code. The match is - * relative to the geographical scope of the message, which determines whether the LAC - * and Cell ID are saved in mLocation or set to -1 to match all values. - * - * @param plmn the current PLMN - * @param lac the current Location Area (GSM) or Service Area (UMTS) - * @param cid the current Cell ID - * @return true if this message is valid for the current location; false otherwise - */ - @UnsupportedAppUsage - public boolean matchesLocation(String plmn, int lac, int cid) { - return mLocation.isInLocationArea(plmn, lac, cid); - } - } -} diff --git a/src/com/android/cellbroadcastreceiver/GsmSmsCbMessage.java b/src/com/android/cellbroadcastreceiver/GsmSmsCbMessage.java deleted file mode 100644 index 240ac2d6e..000000000 --- a/src/com/android/cellbroadcastreceiver/GsmSmsCbMessage.java +++ /dev/null @@ -1,509 +0,0 @@ -/* - * Copyright (C) 2012 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.cellbroadcastreceiver; - -import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE; -import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI; -import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY; -import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE; -import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI; - -import android.annotation.NonNull; -import android.content.Context; -import android.content.res.Resources; -import android.telephony.SmsCbLocation; -import android.telephony.SmsCbMessage; -import android.util.Pair; -import android.util.Slog; - -import com.android.cellbroadcastreceiver.CbGeoUtils.Circle; -import com.android.cellbroadcastreceiver.CbGeoUtils.Polygon; -import com.android.cellbroadcastreceiver.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity; -import com.android.cellbroadcastreceiver.SmsCbHeader.DataCodingScheme; -import com.android.internal.R; -import com.android.internal.telephony.CbGeoUtils.Geometry; -import com.android.internal.telephony.CbGeoUtils.LatLng; -import com.android.internal.telephony.GsmAlphabet; -import com.android.internal.telephony.SmsConstants; - -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is - * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases. - */ -public class GsmSmsCbMessage { - private static final String TAG = GsmSmsCbMessage.class.getSimpleName(); - - private static final char CARRIAGE_RETURN = 0x0d; - - private static final int PDU_BODY_PAGE_LENGTH = 82; - - /** Utility class with only static methods. */ - private GsmSmsCbMessage() { } - - /** - * Get built-in ETWS primary messages by category. ETWS primary message does not contain text, - * so we have to show the pre-built messages to the user. - * - * @param context Device context - * @param category ETWS message category defined in SmsCbConstants - * @return ETWS text message in string. Return an empty string if no match. - */ - private static String getEtwsPrimaryMessage(Context context, int category) { - final Resources r = context.getResources(); - switch (category) { - case ETWS_WARNING_TYPE_EARTHQUAKE: - return r.getString(R.string.etws_primary_default_message_earthquake); - case ETWS_WARNING_TYPE_TSUNAMI: - return r.getString(R.string.etws_primary_default_message_tsunami); - case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI: - return r.getString(R.string.etws_primary_default_message_earthquake_and_tsunami); - case ETWS_WARNING_TYPE_TEST_MESSAGE: - return r.getString(R.string.etws_primary_default_message_test); - case ETWS_WARNING_TYPE_OTHER_EMERGENCY: - return r.getString(R.string.etws_primary_default_message_others); - default: - return ""; - } - } - - /** - * Create a new SmsCbMessage object from a header object plus one or more received PDUs. - * - * @param pdus PDU bytes - */ - public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header, - SmsCbLocation location, byte[][] pdus) - throws IllegalArgumentException { - long receivedTimeMillis = System.currentTimeMillis(); - if (header.isEtwsPrimaryNotification()) { - // ETSI TS 23.041 ETWS Primary Notification message - // ETWS primary message only contains 4 fields including serial number, - // message identifier, warning type, and warning security information. - // There is no field for the content/text so we get the text from the resources. - return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(), - header.getSerialNumber(), location, header.getServiceCategory(), null, - getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()), - SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(), - header.getCmasInfo(), 0, null /* geometries */, receivedTimeMillis); - } else if (header.isUmtsFormat()) { - // UMTS format has only 1 PDU - byte[] pdu = pdus[0]; - Pair<String, String> cbData = parseUmtsBody(header, pdu); - String language = cbData.first; - String body = cbData.second; - int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY - : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; - int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; - int wacDataOffset = SmsCbHeader.PDU_HEADER_LENGTH - + 1 // number of pages - + (PDU_BODY_PAGE_LENGTH + 1) * nrPages; // cb data - - // Has Warning Area Coordinates information - List<Geometry> geometries = null; - int maximumWaitingTimeSec = 255; - if (pdu.length > wacDataOffset) { - try { - Pair<Integer, List<Geometry>> wac = parseWarningAreaCoordinates(pdu, - wacDataOffset); - maximumWaitingTimeSec = wac.first; - geometries = wac.second; - } catch (Exception ex) { - // Catch the exception here, the message will be considered as having no WAC - // information which means the message will be broadcasted directly. - Slog.e(TAG, "Can't parse warning area coordinates, ex = " + ex.toString()); - } - } - - return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, - header.getGeographicalScope(), header.getSerialNumber(), location, - header.getServiceCategory(), language, body, priority, - header.getEtwsInfo(), header.getCmasInfo(), maximumWaitingTimeSec, geometries, - receivedTimeMillis); - } else { - String language = null; - StringBuilder sb = new StringBuilder(); - for (byte[] pdu : pdus) { - Pair<String, String> p = parseGsmBody(header, pdu); - language = p.first; - sb.append(p.second); - } - int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY - : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; - - return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, - header.getGeographicalScope(), header.getSerialNumber(), location, - header.getServiceCategory(), language, sb.toString(), priority, - header.getEtwsInfo(), header.getCmasInfo(), 0, null /* geometries */, - receivedTimeMillis); - } - } - - /** - * Parse WEA Handset Action Message(WHAM) a.k.a geo-fencing trigger message. - * - * WEA Handset Action Message(WHAM) is a cell broadcast service message broadcast by the network - * to direct devices to perform a geo-fencing check on selected alerts. - * - * WEA Handset Action Message(WHAM) requirements from ATIS-0700041 section 4 - * 1. The Warning Message contents of a WHAM shall be in Cell Broadcast(CB) data format as - * defined in TS 23.041. - * 2. The Warning Message Contents of WHAM shall be limited to one CB page(max 20 referenced - * WEA messages). - * 3. The broadcast area for a WHAM shall be the union of the broadcast areas of the referenced - * WEA message. - * @param pdu cell broadcast pdu, including the header - * @return {@link GeoFencingTriggerMessage} instance - */ - public static GeoFencingTriggerMessage createGeoFencingTriggerMessage(byte[] pdu) { - try { - // Header length + 1(number of page). ATIS-0700041 define the number of page of - // geo-fencing trigger message is 1. - int whamOffset = SmsCbHeader.PDU_HEADER_LENGTH + 1; - - BitStreamReader bitReader = new BitStreamReader(pdu, whamOffset); - int type = bitReader.read(4); - int length = bitReader.read(7); - // Skip the remained 5 bits - bitReader.skip(); - - int messageIdentifierCount = (length - 2) * 8 / 32; - List<CellBroadcastIdentity> cbIdentifiers = new ArrayList<>(); - for (int i = 0; i < messageIdentifierCount; i++) { - // Both messageIdentifier and serialNumber are 16 bits integers. - // ATIS-0700041 Section 5.1.6 - int messageIdentifier = bitReader.read(16); - int serialNumber = bitReader.read(16); - cbIdentifiers.add(new CellBroadcastIdentity(messageIdentifier, serialNumber)); - } - return new GeoFencingTriggerMessage(type, cbIdentifiers); - } catch (Exception ex) { - Slog.e(TAG, "create geo-fencing trigger failed, ex = " + ex.toString()); - return null; - } - } - - /** - * Parse the broadcast area and maximum wait time from the Warning Area Coordinates TLV. - * - * @param pdu Warning Area Coordinates TLV. - * @param wacOffset the offset of Warning Area Coordinates TLV. - * @return a pair with the first element is maximum wait time and the second is the broadcast - * area. The default value of the maximum wait time is 255 which means use the device default - * value. - */ - private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates( - byte[] pdu, int wacOffset) { - // little-endian - int wacDataLength = (pdu[wacOffset + 1] << 8) | pdu[wacOffset]; - int offset = wacOffset + 2; - - if (offset + wacDataLength > pdu.length) { - throw new IllegalArgumentException("Invalid wac data, expected the length of pdu at" - + "least " + offset + wacDataLength + ", actual is " + pdu.length); - } - - BitStreamReader bitReader = new BitStreamReader(pdu, offset); - - int maximumWaitTimeSec = SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET; - - List<Geometry> geo = new ArrayList<>(); - int remainedBytes = wacDataLength; - while (remainedBytes > 0) { - int type = bitReader.read(4); - int length = bitReader.read(10); - remainedBytes -= length; - // Skip the 2 remained bits - bitReader.skip(); - - switch (type) { - case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME: - maximumWaitTimeSec = bitReader.read(8); - break; - case CbGeoUtils.GEOMETRY_TYPE_POLYGON: - List<LatLng> latLngs = new ArrayList<>(); - // Each coordinate is represented by 44 bits integer. - // ATIS-0700041 5.2.4 Coordinate coding - int n = (length - 2) * 8 / 44; - for (int i = 0; i < n; i++) { - latLngs.add(getLatLng(bitReader)); - } - // Skip the padding bits - bitReader.skip(); - geo.add(new Polygon(latLngs)); - break; - case CbGeoUtils.GEOMETRY_TYPE_CIRCLE: - LatLng center = getLatLng(bitReader); - // radius = (wacRadius / 2^6). The unit of wacRadius is km, we use meter as the - // distance unit during geo-fencing. - // ATIS-0700041 5.2.5 radius coding - double radius = (bitReader.read(20) * 1.0 / (1 << 6)) * 1000.0; - geo.add(new Circle(center, radius)); - break; - default: - throw new IllegalArgumentException("Unsupported geoType = " + type); - } - } - return new Pair(maximumWaitTimeSec, geo); - } - - /** - * The coordinate is (latitude, longitude), represented by a 44 bits integer. - * The coding is defined in ATIS-0700041 5.2.4 - * @param bitReader - * @return coordinate (latitude, longitude) - */ - private static LatLng getLatLng(BitStreamReader bitReader) { - // wacLatitude = floor(((latitude + 90) / 180) * 2^22) - // wacLongitude = floor(((longitude + 180) / 360) * 2^22) - int wacLat = bitReader.read(22); - int wacLng = bitReader.read(22); - - // latitude = wacLatitude * 180 / 2^22 - 90 - // longitude = wacLongitude * 360 / 2^22 -180 - return new LatLng((wacLat * 180.0 / (1 << 22)) - 90, (wacLng * 360.0 / (1 << 22) - 180)); - } - - /** - * Parse and unpack the UMTS body text according to the encoding in the data coding scheme. - * - * @param header the message header to use - * @param pdu the PDU to decode - * @return a pair of string containing the language and body of the message in order - */ - private static Pair<String, String> parseUmtsBody(SmsCbHeader header, byte[] pdu) { - // Payload may contain multiple pages - int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; - String language = header.getDataCodingSchemeStructedData().language; - - if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) - * nrPages) { - throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " - + nrPages + " pages"); - } - - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < nrPages; i++) { - // Each page is 82 bytes followed by a length octet indicating - // the number of useful octets within those 82 - int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; - int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; - - if (length > PDU_BODY_PAGE_LENGTH) { - throw new IllegalArgumentException("Page length " + length - + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); - } - - Pair<String, String> p = unpackBody(pdu, offset, length, - header.getDataCodingSchemeStructedData()); - language = p.first; - sb.append(p.second); - } - return new Pair(language, sb.toString()); - - } - - /** - * Parse and unpack the GSM body text according to the encoding in the data coding scheme. - * @param header the message header to use - * @param pdu the PDU to decode - * @return a pair of string containing the language and body of the message in order - */ - private static Pair<String, String> parseGsmBody(SmsCbHeader header, byte[] pdu) { - // Payload is one single page - int offset = SmsCbHeader.PDU_HEADER_LENGTH; - int length = pdu.length - offset; - return unpackBody(pdu, offset, length, header.getDataCodingSchemeStructedData()); - } - - /** - * Unpack body text from the pdu using the given encoding, position and length within the pdu. - * - * @param pdu The pdu - * @param offset Position of the first byte to unpack - * @param length Number of bytes to unpack - * @param dcs data coding scheme - * @return a Pair of Strings containing the language and body of the message - */ - private static Pair<String, String> unpackBody(byte[] pdu, int offset, int length, - DataCodingScheme dcs) { - String body = null; - - String language = dcs.language; - switch (dcs.encoding) { - case SmsConstants.ENCODING_7BIT: - body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); - - if (dcs.hasLanguageIndicator && body != null && body.length() > 2) { - // Language is two GSM characters followed by a CR. - // The actual body text is offset by 3 characters. - language = body.substring(0, 2); - body = body.substring(3); - } - break; - - case SmsConstants.ENCODING_16BIT: - if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) { - // Language is two GSM characters. - // The actual body text is offset by 2 bytes. - language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); - offset += 2; - length -= 2; - } - - try { - body = new String(pdu, offset, (length & 0xfffe), "utf-16"); - } catch (UnsupportedEncodingException e) { - // Apparently it wasn't valid UTF-16. - throw new IllegalArgumentException("Error decoding UTF-16 message", e); - } - break; - - default: - break; - } - - if (body != null) { - // Remove trailing carriage return - for (int i = body.length() - 1; i >= 0; i--) { - if (body.charAt(i) != CARRIAGE_RETURN) { - body = body.substring(0, i + 1); - break; - } - } - } else { - body = ""; - } - - return new Pair<String, String>(language, body); - } - - /** A class use to facilitate the processing of bits stream data. */ - private static final class BitStreamReader { - /** The bits stream represent by a bytes array. */ - private final byte[] mData; - - /** The offset of the current byte. */ - private int mCurrentOffset; - - /** - * The remained bits of the current byte which have not been read. The most significant - * will be read first, so the remained bits are always the least significant bits. - */ - private int mRemainedBit; - - /** - * Constructor - * @param data bit stream data represent by byte array. - * @param offset the offset of the first byte. - */ - BitStreamReader(byte[] data, int offset) { - mData = data; - mCurrentOffset = offset; - mRemainedBit = 8; - } - - /** - * Read the first {@code count} bits. - * @param count the number of bits need to read - * @return {@code bits} represent by an 32-bits integer, therefore {@code count} must be no - * greater than 32. - */ - public int read(int count) throws IndexOutOfBoundsException { - int val = 0; - while (count > 0) { - if (count >= mRemainedBit) { - val <<= mRemainedBit; - val |= mData[mCurrentOffset] & ((1 << mRemainedBit) - 1); - count -= mRemainedBit; - mRemainedBit = 8; - ++mCurrentOffset; - } else { - val <<= count; - val |= (mData[mCurrentOffset] & ((1 << mRemainedBit) - 1)) - >> (mRemainedBit - count); - mRemainedBit -= count; - count = 0; - } - } - return val; - } - - /** - * Skip the current bytes if the remained bits is less than 8. This is useful when - * processing the padding or reserved bits. - */ - public void skip() { - if (mRemainedBit < 8) { - mRemainedBit = 8; - ++mCurrentOffset; - } - } - } - - /** - * Part of a GSM SMS cell broadcast message which may trigger geo-fencing logic. - * @hide - */ - public static final class GeoFencingTriggerMessage { - /** - * Indicate the list of active alerts share their warning area coordinates which means the - * broadcast area is the union of the broadcast areas of the active alerts in this list. - */ - public static final int TYPE_ACTIVE_ALERT_SHARE_WAC = 2; - - public final int type; - public final List<CellBroadcastIdentity> cbIdentifiers; - - GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers) { - this.type = type; - this.cbIdentifiers = cbIdentifiers; - } - - /** - * Whether the trigger message indicates that the broadcast areas are shared between all - * active alerts. - * @return true if broadcast areas are to be shared - */ - boolean shouldShareBroadcastArea() { - return type == TYPE_ACTIVE_ALERT_SHARE_WAC; - } - - static final class CellBroadcastIdentity { - public final int messageIdentifier; - public final int serialNumber; - CellBroadcastIdentity(int messageIdentifier, int serialNumber) { - this.messageIdentifier = messageIdentifier; - this.serialNumber = serialNumber; - } - } - - @Override - public String toString() { - String identifiers = cbIdentifiers.stream() - .map(cbIdentifier ->String.format("(msgId = %d, serial = %d)", - cbIdentifier.messageIdentifier, cbIdentifier.serialNumber)) - .collect(Collectors.joining(",")); - return "triggerType=" + type + " identifiers=" + identifiers; - } - } -} diff --git a/src/com/android/cellbroadcastreceiver/IState.java b/src/com/android/cellbroadcastreceiver/IState.java deleted file mode 100644 index 31c2e17b6..000000000 --- a/src/com/android/cellbroadcastreceiver/IState.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (C) 2011 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.cellbroadcastreceiver; - -import android.annotation.UnsupportedAppUsage; -import android.os.Message; - -/** - * {@hide} - * - * The interface for implementing states in a {@link StateMachine} - */ -public interface IState { - - /** - * Returned by processMessage to indicate the the message was processed. - */ - boolean HANDLED = true; - - /** - * Returned by processMessage to indicate the the message was NOT processed. - */ - boolean NOT_HANDLED = false; - - /** - * Called when a state is entered. - */ - void enter(); - - /** - * Called when a state is exited. - */ - void exit(); - - /** - * Called when a message is to be processed by the - * state machine. - * - * This routine is never reentered thus no synchronization - * is needed as only one processMessage method will ever be - * executing within a state machine at any given time. This - * does mean that processing by this routine must be completed - * as expeditiously as possible as no subsequent messages will - * be processed until this routine returns. - * - * @param msg to process - * @return HANDLED if processing has completed and NOT_HANDLED - * if the message wasn't processed. - */ - boolean processMessage(Message msg); - - /** - * Name of State for debugging purposes. - * - * @return name of state. - */ - @UnsupportedAppUsage - String getName(); -} diff --git a/src/com/android/cellbroadcastreceiver/SmsCbHeader.java b/src/com/android/cellbroadcastreceiver/SmsCbHeader.java deleted file mode 100644 index 6210c8070..000000000 --- a/src/com/android/cellbroadcastreceiver/SmsCbHeader.java +++ /dev/null @@ -1,596 +0,0 @@ -/* - * Copyright (C) 2010 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.cellbroadcastreceiver; - -import android.telephony.SmsCbCmasInfo; -import android.telephony.SmsCbEtwsInfo; - -import com.android.internal.telephony.SmsConstants; -import com.android.internal.telephony.gsm.SmsCbConstants; - -import dalvik.annotation.compat.UnsupportedAppUsage; - -import java.util.Arrays; -import java.util.Locale; - -/** - * Parses a 3GPP TS 23.041 cell broadcast message header. This class is public for use by - * CellBroadcastReceiver test cases, but should not be used by applications. - * - * All relevant header information is now sent as a Parcelable - * {@link android.telephony.SmsCbMessage} object in the "message" extra of the - * {@link android.provider.Telephony.Sms.Intents#SMS_CB_RECEIVED_ACTION} or - * {@link android.provider.Telephony.Sms.Intents#SMS_EMERGENCY_CB_RECEIVED_ACTION} intent. - * The raw PDU is no longer sent to SMS CB applications. - */ -public class SmsCbHeader { - /** - * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5. - */ - private static final String[] LANGUAGE_CODES_GROUP_0 = { - Locale.GERMAN.getLanguage(), // German - Locale.ENGLISH.getLanguage(), // English - Locale.ITALIAN.getLanguage(), // Italian - Locale.FRENCH.getLanguage(), // French - new Locale("es").getLanguage(), // Spanish - new Locale("nl").getLanguage(), // Dutch - new Locale("sv").getLanguage(), // Swedish - new Locale("da").getLanguage(), // Danish - new Locale("pt").getLanguage(), // Portuguese - new Locale("fi").getLanguage(), // Finnish - new Locale("nb").getLanguage(), // Norwegian - new Locale("el").getLanguage(), // Greek - new Locale("tr").getLanguage(), // Turkish - new Locale("hu").getLanguage(), // Hungarian - new Locale("pl").getLanguage(), // Polish - null - }; - - /** - * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5. - */ - private static final String[] LANGUAGE_CODES_GROUP_2 = { - new Locale("cs").getLanguage(), // Czech - new Locale("he").getLanguage(), // Hebrew - new Locale("ar").getLanguage(), // Arabic - new Locale("ru").getLanguage(), // Russian - new Locale("is").getLanguage(), // Icelandic - null, null, null, null, null, null, null, null, null, null, null - }; - - /** - * Length of SMS-CB header - */ - static final int PDU_HEADER_LENGTH = 6; - - /** - * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1 - */ - static final int FORMAT_GSM = 1; - - /** - * UMTS pdu format, as defined in 3gpp TS 23.041, section 9.4.2 - */ - static final int FORMAT_UMTS = 2; - - /** - * ETWS pdu format, as defined in 3gpp TS 23.041, section 9.4.1.3 - */ - static final int FORMAT_ETWS_PRIMARY = 3; - - /** - * Message type value as defined in 3gpp TS 25.324, section 11.1. - */ - private static final int MESSAGE_TYPE_CBS_MESSAGE = 1; - - /** - * Length of GSM pdus - */ - private static final int PDU_LENGTH_GSM = 88; - - /** - * Maximum length of ETWS primary message GSM pdus - */ - private static final int PDU_LENGTH_ETWS = 56; - - private final int mGeographicalScope; - - /** The serial number combines geographical scope, message code, and update number. */ - private final int mSerialNumber; - - /** The Message Identifier in 3GPP is the same as the Service Category in CDMA. */ - @UnsupportedAppUsage - private final int mMessageIdentifier; - - private final int mDataCodingScheme; - - private final int mPageIndex; - - private final int mNrOfPages; - - private final int mFormat; - - private DataCodingScheme mDataCodingSchemeStructedData; - - /** ETWS warning notification info. */ - private final SmsCbEtwsInfo mEtwsInfo; - - /** CMAS warning notification info. */ - private final SmsCbCmasInfo mCmasInfo; - - @UnsupportedAppUsage - public SmsCbHeader(byte[] pdu) throws IllegalArgumentException { - if (pdu == null || pdu.length < PDU_HEADER_LENGTH) { - throw new IllegalArgumentException("Illegal PDU"); - } - - if (pdu.length <= PDU_LENGTH_GSM) { - // can be ETWS or GSM format. - // Per TS23.041 9.4.1.2 and 9.4.1.3.2, GSM and ETWS format both - // contain serial number which contains GS, Message Code, and Update Number - // per 9.4.1.2.1, and message identifier in same octets - mGeographicalScope = (pdu[0] & 0xc0) >>> 6; - mSerialNumber = ((pdu[0] & 0xff) << 8) | (pdu[1] & 0xff); - mMessageIdentifier = ((pdu[2] & 0xff) << 8) | (pdu[3] & 0xff); - if (isEtwsMessage() && pdu.length <= PDU_LENGTH_ETWS) { - mFormat = FORMAT_ETWS_PRIMARY; - mDataCodingScheme = -1; - mPageIndex = -1; - mNrOfPages = -1; - boolean emergencyUserAlert = (pdu[4] & 0x1) != 0; - boolean activatePopup = (pdu[5] & 0x80) != 0; - int warningType = (pdu[4] & 0xfe) >>> 1; - byte[] warningSecurityInfo; - // copy the Warning-Security-Information, if present - if (pdu.length > PDU_HEADER_LENGTH) { - warningSecurityInfo = Arrays.copyOfRange(pdu, 6, pdu.length); - } else { - warningSecurityInfo = null; - } - mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup, - true, warningSecurityInfo); - mCmasInfo = null; - return; // skip the ETWS/CMAS initialization code for regular notifications - } else { - // GSM pdus are no more than 88 bytes - mFormat = FORMAT_GSM; - mDataCodingScheme = pdu[4] & 0xff; - - // Check for invalid page parameter - int pageIndex = (pdu[5] & 0xf0) >>> 4; - int nrOfPages = pdu[5] & 0x0f; - - if (pageIndex == 0 || nrOfPages == 0 || pageIndex > nrOfPages) { - pageIndex = 1; - nrOfPages = 1; - } - - mPageIndex = pageIndex; - mNrOfPages = nrOfPages; - } - } else { - // UMTS pdus are always at least 90 bytes since the payload includes - // a number-of-pages octet and also one length octet per page - mFormat = FORMAT_UMTS; - - int messageType = pdu[0]; - - if (messageType != MESSAGE_TYPE_CBS_MESSAGE) { - throw new IllegalArgumentException("Unsupported message type " + messageType); - } - - mMessageIdentifier = ((pdu[1] & 0xff) << 8) | pdu[2] & 0xff; - mGeographicalScope = (pdu[3] & 0xc0) >>> 6; - mSerialNumber = ((pdu[3] & 0xff) << 8) | (pdu[4] & 0xff); - mDataCodingScheme = pdu[5] & 0xff; - - // We will always consider a UMTS message as having one single page - // since there's only one instance of the header, even though the - // actual payload may contain several pages. - mPageIndex = 1; - mNrOfPages = 1; - } - - if (mDataCodingScheme != -1) { - mDataCodingSchemeStructedData = new DataCodingScheme(mDataCodingScheme); - } - - if (isEtwsMessage()) { - boolean emergencyUserAlert = isEtwsEmergencyUserAlert(); - boolean activatePopup = isEtwsPopupAlert(); - int warningType = getEtwsWarningType(); - mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup, - false, null); - mCmasInfo = null; - } else if (isCmasMessage()) { - int messageClass = getCmasMessageClass(); - int severity = getCmasSeverity(); - int urgency = getCmasUrgency(); - int certainty = getCmasCertainty(); - mEtwsInfo = null; - mCmasInfo = new SmsCbCmasInfo(messageClass, SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, - SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, severity, urgency, certainty); - } else { - mEtwsInfo = null; - mCmasInfo = null; - } - } - - @UnsupportedAppUsage - int getGeographicalScope() { - return mGeographicalScope; - } - - @UnsupportedAppUsage - int getSerialNumber() { - return mSerialNumber; - } - - @UnsupportedAppUsage - int getServiceCategory() { - return mMessageIdentifier; - } - - int getDataCodingScheme() { - return mDataCodingScheme; - } - - DataCodingScheme getDataCodingSchemeStructedData() { - return mDataCodingSchemeStructedData; - } - - @UnsupportedAppUsage - int getPageIndex() { - return mPageIndex; - } - - @UnsupportedAppUsage - int getNumberOfPages() { - return mNrOfPages; - } - - SmsCbEtwsInfo getEtwsInfo() { - return mEtwsInfo; - } - - SmsCbCmasInfo getCmasInfo() { - return mCmasInfo; - } - - /** - * Return whether this broadcast is an emergency (PWS) message type. - * @return true if this message is emergency type; false otherwise - */ - boolean isEmergencyMessage() { - return mMessageIdentifier >= SmsCbConstants.MESSAGE_ID_PWS_FIRST_IDENTIFIER - && mMessageIdentifier <= SmsCbConstants.MESSAGE_ID_PWS_LAST_IDENTIFIER; - } - - /** - * Return whether this broadcast is an ETWS emergency message type. - * @return true if this message is ETWS emergency type; false otherwise - */ - private boolean isEtwsMessage() { - return (mMessageIdentifier & SmsCbConstants.MESSAGE_ID_ETWS_TYPE_MASK) - == SmsCbConstants.MESSAGE_ID_ETWS_TYPE; - } - - /** - * Return whether this broadcast is an ETWS primary notification. - * @return true if this message is an ETWS primary notification; false otherwise - */ - boolean isEtwsPrimaryNotification() { - return mFormat == FORMAT_ETWS_PRIMARY; - } - - /** - * Return whether this broadcast is in UMTS format. - * @return true if this message is in UMTS format; false otherwise - */ - boolean isUmtsFormat() { - return mFormat == FORMAT_UMTS; - } - - /** - * Return whether this message is a CMAS emergency message type. - * @return true if this message is CMAS emergency type; false otherwise - */ - private boolean isCmasMessage() { - return mMessageIdentifier >= SmsCbConstants.MESSAGE_ID_CMAS_FIRST_IDENTIFIER - && mMessageIdentifier <= SmsCbConstants.MESSAGE_ID_CMAS_LAST_IDENTIFIER; - } - - /** - * Return whether the popup alert flag is set for an ETWS warning notification. - * This method assumes that the message ID has already been checked for ETWS type. - * - * @return true if the message code indicates a popup alert should be displayed - */ - private boolean isEtwsPopupAlert() { - return (mSerialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_ACTIVATE_POPUP) != 0; - } - - /** - * Return whether the emergency user alert flag is set for an ETWS warning notification. - * This method assumes that the message ID has already been checked for ETWS type. - * - * @return true if the message code indicates an emergency user alert - */ - private boolean isEtwsEmergencyUserAlert() { - return (mSerialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT) != 0; - } - - /** - * Returns the warning type for an ETWS warning notification. - * This method assumes that the message ID has already been checked for ETWS type. - * - * @return the ETWS warning type defined in 3GPP TS 23.041 section 9.3.24 - */ - private int getEtwsWarningType() { - return mMessageIdentifier - SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING; - } - - /** - * Returns the message class for a CMAS warning notification. - * This method assumes that the message ID has already been checked for CMAS type. - * @return the CMAS message class as defined in {@link SmsCbCmasInfo} - */ - private int getCmasMessageClass() { - switch (mMessageIdentifier) { - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE: - return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE: - return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE: - return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE: - return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST_LANGUAGE: - return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE_LANGUAGE: - return SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE_LANGUAGE: - return SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE; - - default: - return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; - } - } - - /** - * Returns the severity for a CMAS warning notification. This is only available for extreme - * and severe alerts, not for other types such as Presidential Level and AMBER alerts. - * This method assumes that the message ID has already been checked for CMAS type. - * @return the CMAS severity as defined in {@link SmsCbCmasInfo} - */ - private int getCmasSeverity() { - switch (mMessageIdentifier) { - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE: - return SmsCbCmasInfo.CMAS_SEVERITY_EXTREME; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE: - return SmsCbCmasInfo.CMAS_SEVERITY_SEVERE; - - default: - return SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; - } - } - - /** - * Returns the urgency for a CMAS warning notification. This is only available for extreme - * and severe alerts, not for other types such as Presidential Level and AMBER alerts. - * This method assumes that the message ID has already been checked for CMAS type. - * @return the CMAS urgency as defined in {@link SmsCbCmasInfo} - */ - private int getCmasUrgency() { - switch (mMessageIdentifier) { - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE: - return SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE: - return SmsCbCmasInfo.CMAS_URGENCY_EXPECTED; - - default: - return SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; - } - } - - /** - * Returns the certainty for a CMAS warning notification. This is only available for extreme - * and severe alerts, not for other types such as Presidential Level and AMBER alerts. - * This method assumes that the message ID has already been checked for CMAS type. - * @return the CMAS certainty as defined in {@link SmsCbCmasInfo} - */ - private int getCmasCertainty() { - switch (mMessageIdentifier) { - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED_LANGUAGE: - return SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED; - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY_LANGUAGE: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE: - return SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY; - - default: - return SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; - } - } - - @Override - public String toString() { - return "SmsCbHeader{GS=" + mGeographicalScope + ", serialNumber=0x" - + Integer.toHexString(mSerialNumber) - + ", messageIdentifier=0x" + Integer.toHexString(mMessageIdentifier) - + ", format=" + mFormat - + ", DCS=0x" + Integer.toHexString(mDataCodingScheme) - + ", page " + mPageIndex + " of " + mNrOfPages + '}'; - } - - /** - * CBS Data Coding Scheme. - * Reference: 3GPP TS 23.038 version 15.0.0 section #5, CBS Data Coding Scheme - */ - public static final class DataCodingScheme { - public final int encoding; - public final String language; - public final boolean hasLanguageIndicator; - - public DataCodingScheme(int dataCodingScheme) { - int encoding = 0; - String language = null; - boolean hasLanguageIndicator = false; - - // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, - // section 5. - switch ((dataCodingScheme & 0xf0) >> 4) { - case 0x00: - encoding = SmsConstants.ENCODING_7BIT; - language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f]; - break; - - case 0x01: - hasLanguageIndicator = true; - if ((dataCodingScheme & 0x0f) == 0x01) { - encoding = SmsConstants.ENCODING_16BIT; - } else { - encoding = SmsConstants.ENCODING_7BIT; - } - break; - - case 0x02: - encoding = SmsConstants.ENCODING_7BIT; - language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f]; - break; - - case 0x03: - encoding = SmsConstants.ENCODING_7BIT; - break; - - case 0x04: - case 0x05: - switch ((dataCodingScheme & 0x0c) >> 2) { - case 0x01: - encoding = SmsConstants.ENCODING_8BIT; - break; - - case 0x02: - encoding = SmsConstants.ENCODING_16BIT; - break; - - case 0x00: - default: - encoding = SmsConstants.ENCODING_7BIT; - break; - } - break; - - case 0x06: - case 0x07: - // Compression not supported - case 0x09: - // UDH structure not supported - case 0x0e: - // Defined by the WAP forum not supported - throw new IllegalArgumentException("Unsupported GSM dataCodingScheme " - + dataCodingScheme); - - case 0x0f: - if (((dataCodingScheme & 0x04) >> 2) == 0x01) { - encoding = SmsConstants.ENCODING_8BIT; - } else { - encoding = SmsConstants.ENCODING_7BIT; - } - break; - - default: - // Reserved values are to be treated as 7-bit - encoding = SmsConstants.ENCODING_7BIT; - break; - } - - - this.encoding = encoding; - this.language = language; - this.hasLanguageIndicator = hasLanguageIndicator; - } - } -} diff --git a/src/com/android/cellbroadcastreceiver/State.java b/src/com/android/cellbroadcastreceiver/State.java deleted file mode 100644 index 32e0db07a..000000000 --- a/src/com/android/cellbroadcastreceiver/State.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (C) 2009 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.cellbroadcastreceiver; - -import android.annotation.UnsupportedAppUsage; -import android.os.Message; - -/** - * {@hide} - * - * The class for implementing states in a StateMachine - */ -public class State implements IState { - - /** - * Constructor - */ - @UnsupportedAppUsage - protected State() { - } - - /* (non-Javadoc) - * @see com.android.internal.util.IState#enter() - */ - @UnsupportedAppUsage - @Override - public void enter() { - } - - /* (non-Javadoc) - * @see com.android.internal.util.IState#exit() - */ - @UnsupportedAppUsage - @Override - public void exit() { - } - - /* (non-Javadoc) - * @see com.android.internal.util.IState#processMessage(android.os.Message) - */ - @UnsupportedAppUsage - @Override - public boolean processMessage(Message msg) { - return false; - } - - /** - * Name of State for debugging purposes. - * - * This default implementation returns the class name, returning - * the instance name would better in cases where a State class - * is used for multiple states. But normally there is one class per - * state and the class name is sufficient and easy to get. You may - * want to provide a setName or some other mechanism for setting - * another name if the class name is not appropriate. - * - * @see com.android.internal.util.IState#processMessage(android.os.Message) - */ - @UnsupportedAppUsage - @Override - public String getName() { - String name = getClass().getName(); - int lastDollar = name.lastIndexOf('$'); - return name.substring(lastDollar + 1); - } -} diff --git a/src/com/android/cellbroadcastreceiver/StateMachine.java b/src/com/android/cellbroadcastreceiver/StateMachine.java deleted file mode 100644 index 2a2fb43b4..000000000 --- a/src/com/android/cellbroadcastreceiver/StateMachine.java +++ /dev/null @@ -1,2183 +0,0 @@ -/** - * Copyright (C) 2009 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.cellbroadcastreceiver; - -import android.annotation.UnsupportedAppUsage; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.os.Message; -import android.text.TextUtils; -import android.util.Log; - -import com.android.internal.annotations.VisibleForTesting; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Vector; - -/** - * {@hide} - * - * <p>The state machine defined here is a hierarchical state machine which processes messages - * and can have states arranged hierarchically.</p> - * - * <p>A state is a <code>State</code> object and must implement - * <code>processMessage</code> and optionally <code>enter/exit/getName</code>. - * The enter/exit methods are equivalent to the construction and destruction - * in Object Oriented programming and are used to perform initialization and - * cleanup of the state respectively. The <code>getName</code> method returns the - * name of the state; the default implementation returns the class name. It may be - * desirable to have <code>getName</code> return the the state instance name instead, - * in particular if a particular state class has multiple instances.</p> - * - * <p>When a state machine is created, <code>addState</code> is used to build the - * hierarchy and <code>setInitialState</code> is used to identify which of these - * is the initial state. After construction the programmer calls <code>start</code> - * which initializes and starts the state machine. The first action the StateMachine - * is to the invoke <code>enter</code> for all of the initial state's hierarchy, - * starting at its eldest parent. The calls to enter will be done in the context - * of the StateMachine's Handler, not in the context of the call to start, and they - * will be invoked before any messages are processed. For example, given the simple - * state machine below, mP1.enter will be invoked and then mS1.enter. Finally, - * messages sent to the state machine will be processed by the current state; - * in our simple state machine below that would initially be mS1.processMessage.</p> -<pre> - mP1 - / \ - mS2 mS1 ----> initial state -</pre> - * <p>After the state machine is created and started, messages are sent to a state - * machine using <code>sendMessage</code> and the messages are created using - * <code>obtainMessage</code>. When the state machine receives a message the - * current state's <code>processMessage</code> is invoked. In the above example - * mS1.processMessage will be invoked first. The state may use <code>transitionTo</code> - * to change the current state to a new state.</p> - * - * <p>Each state in the state machine may have a zero or one parent states. If - * a child state is unable to handle a message it may have the message processed - * by its parent by returning false or NOT_HANDLED. If a message is not handled by - * a child state or any of its ancestors, <code>unhandledMessage</code> will be invoked - * to give one last chance for the state machine to process the message.</p> - * - * <p>When all processing is completed a state machine may choose to call - * <code>transitionToHaltingState</code>. When the current <code>processingMessage</code> - * returns the state machine will transfer to an internal <code>HaltingState</code> - * and invoke <code>halting</code>. Any message subsequently received by the state - * machine will cause <code>haltedProcessMessage</code> to be invoked.</p> - * - * <p>If it is desirable to completely stop the state machine call <code>quit</code> or - * <code>quitNow</code>. These will call <code>exit</code> of the current state and its parents, - * call <code>onQuitting</code> and then exit Thread/Loopers.</p> - * - * <p>In addition to <code>processMessage</code> each <code>State</code> has - * an <code>enter</code> method and <code>exit</code> method which may be overridden.</p> - * - * <p>Since the states are arranged in a hierarchy transitioning to a new state - * causes current states to be exited and new states to be entered. To determine - * the list of states to be entered/exited the common parent closest to - * the current state is found. We then exit from the current state and its - * parent's up to but not including the common parent state and then enter all - * of the new states below the common parent down to the destination state. - * If there is no common parent all states are exited and then the new states - * are entered.</p> - * - * <p>Two other methods that states can use are <code>deferMessage</code> and - * <code>sendMessageAtFrontOfQueue</code>. The <code>sendMessageAtFrontOfQueue</code> sends - * a message but places it on the front of the queue rather than the back. The - * <code>deferMessage</code> causes the message to be saved on a list until a - * transition is made to a new state. At which time all of the deferred messages - * will be put on the front of the state machine queue with the oldest message - * at the front. These will then be processed by the new current state before - * any other messages that are on the queue or might be added later. Both of - * these are protected and may only be invoked from within a state machine.</p> - * - * <p>To illustrate some of these properties we'll use state machine with an 8 - * state hierarchy:</p> -<pre> - mP0 - / \ - mP1 mS0 - / \ - mS2 mS1 - / \ \ - mS3 mS4 mS5 ---> initial state -</pre> - * <p>After starting mS5 the list of active states is mP0, mP1, mS1 and mS5. - * So the order of calling processMessage when a message is received is mS5, - * mS1, mP1, mP0 assuming each processMessage indicates it can't handle this - * message by returning false or NOT_HANDLED.</p> - * - * <p>Now assume mS5.processMessage receives a message it can handle, and during - * the handling determines the machine should change states. It could call - * transitionTo(mS4) and return true or HANDLED. Immediately after returning from - * processMessage the state machine runtime will find the common parent, - * which is mP1. It will then call mS5.exit, mS1.exit, mS2.enter and then - * mS4.enter. The new list of active states is mP0, mP1, mS2 and mS4. So - * when the next message is received mS4.processMessage will be invoked.</p> - * - * <p>Now for some concrete examples, here is the canonical HelloWorld as a state machine. - * It responds with "Hello World" being printed to the log for every message.</p> -<pre> -class HelloWorld extends StateMachine { - HelloWorld(String name) { - super(name); - addState(mState1); - setInitialState(mState1); - } - - public static HelloWorld makeHelloWorld() { - HelloWorld hw = new HelloWorld("hw"); - hw.start(); - return hw; - } - - class State1 extends State { - @Override public boolean processMessage(Message message) { - log("Hello World"); - return HANDLED; - } - } - State1 mState1 = new State1(); -} - -void testHelloWorld() { - HelloWorld hw = makeHelloWorld(); - hw.sendMessage(hw.obtainMessage()); -} -</pre> - * <p>A more interesting state machine is one with four states - * with two independent parent states.</p> -<pre> - mP1 mP2 - / \ - mS2 mS1 -</pre> - * <p>Here is a description of this state machine using pseudo code.</p> - <pre> -state mP1 { - enter { log("mP1.enter"); } - exit { log("mP1.exit"); } - on msg { - CMD_2 { - send(CMD_3); - defer(msg); - transitionTo(mS2); - return HANDLED; - } - return NOT_HANDLED; - } -} - -INITIAL -state mS1 parent mP1 { - enter { log("mS1.enter"); } - exit { log("mS1.exit"); } - on msg { - CMD_1 { - transitionTo(mS1); - return HANDLED; - } - return NOT_HANDLED; - } -} - -state mS2 parent mP1 { - enter { log("mS2.enter"); } - exit { log("mS2.exit"); } - on msg { - CMD_2 { - send(CMD_4); - return HANDLED; - } - CMD_3 { - defer(msg); - transitionTo(mP2); - return HANDLED; - } - return NOT_HANDLED; - } -} - -state mP2 { - enter { - log("mP2.enter"); - send(CMD_5); - } - exit { log("mP2.exit"); } - on msg { - CMD_3, CMD_4 { return HANDLED; } - CMD_5 { - transitionTo(HaltingState); - return HANDLED; - } - return NOT_HANDLED; - } -} -</pre> - * <p>The implementation is below and also in StateMachineTest:</p> -<pre> -class Hsm1 extends StateMachine { - public static final int CMD_1 = 1; - public static final int CMD_2 = 2; - public static final int CMD_3 = 3; - public static final int CMD_4 = 4; - public static final int CMD_5 = 5; - - public static Hsm1 makeHsm1() { - log("makeHsm1 E"); - Hsm1 sm = new Hsm1("hsm1"); - sm.start(); - log("makeHsm1 X"); - return sm; - } - - Hsm1(String name) { - super(name); - log("ctor E"); - - // Add states, use indentation to show hierarchy - addState(mP1); - addState(mS1, mP1); - addState(mS2, mP1); - addState(mP2); - - // Set the initial state - setInitialState(mS1); - log("ctor X"); - } - - class P1 extends State { - @Override public void enter() { - log("mP1.enter"); - } - @Override public boolean processMessage(Message message) { - boolean retVal; - log("mP1.processMessage what=" + message.what); - switch(message.what) { - case CMD_2: - // CMD_2 will arrive in mS2 before CMD_3 - sendMessage(obtainMessage(CMD_3)); - deferMessage(message); - transitionTo(mS2); - retVal = HANDLED; - break; - default: - // Any message we don't understand in this state invokes unhandledMessage - retVal = NOT_HANDLED; - break; - } - return retVal; - } - @Override public void exit() { - log("mP1.exit"); - } - } - - class S1 extends State { - @Override public void enter() { - log("mS1.enter"); - } - @Override public boolean processMessage(Message message) { - log("S1.processMessage what=" + message.what); - if (message.what == CMD_1) { - // Transition to ourself to show that enter/exit is called - transitionTo(mS1); - return HANDLED; - } else { - // Let parent process all other messages - return NOT_HANDLED; - } - } - @Override public void exit() { - log("mS1.exit"); - } - } - - class S2 extends State { - @Override public void enter() { - log("mS2.enter"); - } - @Override public boolean processMessage(Message message) { - boolean retVal; - log("mS2.processMessage what=" + message.what); - switch(message.what) { - case(CMD_2): - sendMessage(obtainMessage(CMD_4)); - retVal = HANDLED; - break; - case(CMD_3): - deferMessage(message); - transitionTo(mP2); - retVal = HANDLED; - break; - default: - retVal = NOT_HANDLED; - break; - } - return retVal; - } - @Override public void exit() { - log("mS2.exit"); - } - } - - class P2 extends State { - @Override public void enter() { - log("mP2.enter"); - sendMessage(obtainMessage(CMD_5)); - } - @Override public boolean processMessage(Message message) { - log("P2.processMessage what=" + message.what); - switch(message.what) { - case(CMD_3): - break; - case(CMD_4): - break; - case(CMD_5): - transitionToHaltingState(); - break; - } - return HANDLED; - } - @Override public void exit() { - log("mP2.exit"); - } - } - - @Override - void onHalting() { - log("halting"); - synchronized (this) { - this.notifyAll(); - } - } - - P1 mP1 = new P1(); - S1 mS1 = new S1(); - S2 mS2 = new S2(); - P2 mP2 = new P2(); -} -</pre> - * <p>If this is executed by sending two messages CMD_1 and CMD_2 - * (Note the synchronize is only needed because we use hsm.wait())</p> -<pre> -Hsm1 hsm = makeHsm1(); -synchronize(hsm) { - hsm.sendMessage(obtainMessage(hsm.CMD_1)); - hsm.sendMessage(obtainMessage(hsm.CMD_2)); - try { - // wait for the messages to be handled - hsm.wait(); - } catch (InterruptedException e) { - loge("exception while waiting " + e.getMessage()); - } -} -</pre> - * <p>The output is:</p> -<pre> -D/hsm1 ( 1999): makeHsm1 E -D/hsm1 ( 1999): ctor E -D/hsm1 ( 1999): ctor X -D/hsm1 ( 1999): mP1.enter -D/hsm1 ( 1999): mS1.enter -D/hsm1 ( 1999): makeHsm1 X -D/hsm1 ( 1999): mS1.processMessage what=1 -D/hsm1 ( 1999): mS1.exit -D/hsm1 ( 1999): mS1.enter -D/hsm1 ( 1999): mS1.processMessage what=2 -D/hsm1 ( 1999): mP1.processMessage what=2 -D/hsm1 ( 1999): mS1.exit -D/hsm1 ( 1999): mS2.enter -D/hsm1 ( 1999): mS2.processMessage what=2 -D/hsm1 ( 1999): mS2.processMessage what=3 -D/hsm1 ( 1999): mS2.exit -D/hsm1 ( 1999): mP1.exit -D/hsm1 ( 1999): mP2.enter -D/hsm1 ( 1999): mP2.processMessage what=3 -D/hsm1 ( 1999): mP2.processMessage what=4 -D/hsm1 ( 1999): mP2.processMessage what=5 -D/hsm1 ( 1999): mP2.exit -D/hsm1 ( 1999): halting -</pre> - */ -public class StateMachine { - // Name of the state machine and used as logging tag - private String mName; - - /** Message.what value when quitting */ - private static final int SM_QUIT_CMD = -1; - - /** Message.what value when initializing */ - private static final int SM_INIT_CMD = -2; - - /** - * Convenience constant that maybe returned by processMessage - * to indicate the the message was processed and is not to be - * processed by parent states - */ - public static final boolean HANDLED = true; - - /** - * Convenience constant that maybe returned by processMessage - * to indicate the the message was NOT processed and is to be - * processed by parent states - */ - public static final boolean NOT_HANDLED = false; - - /** - * StateMachine logging record. - * {@hide} - */ - public static class LogRec { - private StateMachine mSm; - private long mTime; - private int mWhat; - private String mInfo; - private IState mState; - private IState mOrgState; - private IState mDstState; - - /** - * Constructor - * - * @param msg - * @param state the state which handled the message - * @param orgState is the first state the received the message but - * did not processes the message. - * @param transToState is the state that was transitioned to after the message was - * processed. - */ - LogRec(StateMachine sm, Message msg, String info, IState state, IState orgState, - IState transToState) { - update(sm, msg, info, state, orgState, transToState); - } - - /** - * Update the information in the record. - * @param state that handled the message - * @param orgState is the first state the received the message - * @param dstState is the state that was the transition target when logging - */ - public void update(StateMachine sm, Message msg, String info, IState state, IState orgState, - IState dstState) { - mSm = sm; - mTime = System.currentTimeMillis(); - mWhat = (msg != null) ? msg.what : 0; - mInfo = info; - mState = state; - mOrgState = orgState; - mDstState = dstState; - } - - /** - * @return time stamp - */ - public long getTime() { - return mTime; - } - - /** - * @return msg.what - */ - public long getWhat() { - return mWhat; - } - - /** - * @return the command that was executing - */ - public String getInfo() { - return mInfo; - } - - /** - * @return the state that handled this message - */ - public IState getState() { - return mState; - } - - /** - * @return the state destination state if a transition is occurring or null if none. - */ - public IState getDestState() { - return mDstState; - } - - /** - * @return the original state that received the message. - */ - public IState getOriginalState() { - return mOrgState; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("time="); - Calendar c = Calendar.getInstance(); - c.setTimeInMillis(mTime); - sb.append(String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c)); - sb.append(" processed="); - sb.append(mState == null ? "<null>" : mState.getName()); - sb.append(" org="); - sb.append(mOrgState == null ? "<null>" : mOrgState.getName()); - sb.append(" dest="); - sb.append(mDstState == null ? "<null>" : mDstState.getName()); - sb.append(" what="); - String what = mSm != null ? mSm.getWhatToString(mWhat) : ""; - if (TextUtils.isEmpty(what)) { - sb.append(mWhat); - sb.append("(0x"); - sb.append(Integer.toHexString(mWhat)); - sb.append(")"); - } else { - sb.append(what); - } - if (!TextUtils.isEmpty(mInfo)) { - sb.append(" "); - sb.append(mInfo); - } - return sb.toString(); - } - } - - /** - * A list of log records including messages recently processed by the state machine. - * - * The class maintains a list of log records including messages - * recently processed. The list is finite and may be set in the - * constructor or by calling setSize. The public interface also - * includes size which returns the number of recent records, - * count which is the number of records processed since the - * the last setSize, get which returns a record and - * add which adds a record. - */ - private static class LogRecords { - - private static final int DEFAULT_SIZE = 20; - - private Vector<LogRec> mLogRecVector = new Vector<LogRec>(); - private int mMaxSize = DEFAULT_SIZE; - private int mOldestIndex = 0; - private int mCount = 0; - private boolean mLogOnlyTransitions = false; - - /** - * private constructor use add - */ - private LogRecords() { - } - - /** - * Set size of messages to maintain and clears all current records. - * - * @param maxSize number of records to maintain at anyone time. - */ - synchronized void setSize(int maxSize) { - // TODO: once b/28217358 is fixed, add unit tests to verify that these variables are - // cleared after calling this method, and that subsequent calls to get() function as - // expected. - mMaxSize = maxSize; - mOldestIndex = 0; - mCount = 0; - mLogRecVector.clear(); - } - - synchronized void setLogOnlyTransitions(boolean enable) { - mLogOnlyTransitions = enable; - } - - synchronized boolean logOnlyTransitions() { - return mLogOnlyTransitions; - } - - /** - * @return the number of recent records. - */ - synchronized int size() { - return mLogRecVector.size(); - } - - /** - * @return the total number of records processed since size was set. - */ - synchronized int count() { - return mCount; - } - - /** - * Clear the list of records. - */ - synchronized void cleanup() { - mLogRecVector.clear(); - } - - /** - * @return the information on a particular record. 0 is the oldest - * record and size()-1 is the newest record. If the index is to - * large null is returned. - */ - synchronized LogRec get(int index) { - int nextIndex = mOldestIndex + index; - if (nextIndex >= mMaxSize) { - nextIndex -= mMaxSize; - } - if (nextIndex >= size()) { - return null; - } else { - return mLogRecVector.get(nextIndex); - } - } - - /** - * Add a processed message. - * - * @param msg - * @param messageInfo to be stored - * @param state that handled the message - * @param orgState is the first state the received the message but - * did not processes the message. - * @param transToState is the state that was transitioned to after the message was - * processed. - * - */ - synchronized void add(StateMachine sm, Message msg, String messageInfo, IState state, - IState orgState, IState transToState) { - mCount += 1; - if (mLogRecVector.size() < mMaxSize) { - mLogRecVector.add(new LogRec(sm, msg, messageInfo, state, orgState, transToState)); - } else { - LogRec pmi = mLogRecVector.get(mOldestIndex); - mOldestIndex += 1; - if (mOldestIndex >= mMaxSize) { - mOldestIndex = 0; - } - pmi.update(sm, msg, messageInfo, state, orgState, transToState); - } - } - } - - private static class SmHandler extends Handler { - - /** true if StateMachine has quit */ - private boolean mHasQuit = false; - - /** The debug flag */ - private boolean mDbg = false; - - /** The SmHandler object, identifies that message is internal */ - private static final Object sSmHandlerObj = new Object(); - - /** The current message */ - private Message mMsg; - - /** A list of log records including messages this state machine has processed */ - private LogRecords mLogRecords = new LogRecords(); - - /** true if construction of the state machine has not been completed */ - private boolean mIsConstructionCompleted; - - /** Stack used to manage the current hierarchy of states */ - private StateInfo[] mStateStack; - - /** Top of mStateStack */ - private int mStateStackTopIndex = -1; - - /** A temporary stack used to manage the state stack */ - private StateInfo[] mTempStateStack; - - /** The top of the mTempStateStack */ - private int mTempStateStackCount; - - /** State used when state machine is halted */ - private HaltingState mHaltingState = new HaltingState(); - - /** State used when state machine is quitting */ - private QuittingState mQuittingState = new QuittingState(); - - /** Reference to the StateMachine */ - private StateMachine mSm; - - /** - * Information about a state. - * Used to maintain the hierarchy. - */ - private class StateInfo { - /** The state */ - State state; - - /** The parent of this state, null if there is no parent */ - StateInfo parentStateInfo; - - /** True when the state has been entered and on the stack */ - boolean active; - - /** - * Convert StateInfo to string - */ - @Override - public String toString() { - return "state=" + state.getName() + ",active=" + active + ",parent=" - + ((parentStateInfo == null) ? "null" : parentStateInfo.state.getName()); - } - } - - /** The map of all of the states in the state machine */ - private HashMap<State, StateInfo> mStateInfo = new HashMap<State, StateInfo>(); - - /** The initial state that will process the first message */ - private State mInitialState; - - /** The destination state when transitionTo has been invoked */ - private State mDestState; - - /** - * Indicates if a transition is in progress - * - * This will be true for all calls of State.exit and all calls of State.enter except for the - * last enter call for the current destination state. - */ - private boolean mTransitionInProgress = false; - - /** The list of deferred messages */ - private ArrayList<Message> mDeferredMessages = new ArrayList<Message>(); - - /** - * State entered when transitionToHaltingState is called. - */ - private class HaltingState extends State { - @Override - public boolean processMessage(Message msg) { - mSm.haltedProcessMessage(msg); - return true; - } - } - - /** - * State entered when a valid quit message is handled. - */ - private class QuittingState extends State { - @Override - public boolean processMessage(Message msg) { - return NOT_HANDLED; - } - } - - /** - * Handle messages sent to the state machine by calling - * the current state's processMessage. It also handles - * the enter/exit calls and placing any deferred messages - * back onto the queue when transitioning to a new state. - */ - @Override - public final void handleMessage(Message msg) { - if (!mHasQuit) { - if (mSm != null && msg.what != SM_INIT_CMD && msg.what != SM_QUIT_CMD) { - mSm.onPreHandleMessage(msg); - } - - if (mDbg) mSm.log("handleMessage: E msg.what=" + msg.what); - - /** Save the current message */ - mMsg = msg; - - /** State that processed the message */ - State msgProcessedState = null; - if (mIsConstructionCompleted || (mMsg.what == SM_QUIT_CMD)) { - /** Normal path */ - msgProcessedState = processMsg(msg); - } else if (!mIsConstructionCompleted && (mMsg.what == SM_INIT_CMD) - && (mMsg.obj == sSmHandlerObj)) { - /** Initial one time path. */ - mIsConstructionCompleted = true; - invokeEnterMethods(0); - } else { - throw new RuntimeException("StateMachine.handleMessage: " - + "The start method not called, received msg: " + msg); - } - performTransitions(msgProcessedState, msg); - - // We need to check if mSm == null here as we could be quitting. - if (mDbg && mSm != null) mSm.log("handleMessage: X"); - - if (mSm != null && msg.what != SM_INIT_CMD && msg.what != SM_QUIT_CMD) { - mSm.onPostHandleMessage(msg); - } - } - } - - /** - * Do any transitions - * @param msgProcessedState is the state that processed the message - */ - private void performTransitions(State msgProcessedState, Message msg) { - /** - * If transitionTo has been called, exit and then enter - * the appropriate states. We loop on this to allow - * enter and exit methods to use transitionTo. - */ - State orgState = mStateStack[mStateStackTopIndex].state; - - /** - * Record whether message needs to be logged before we transition and - * and we won't log special messages SM_INIT_CMD or SM_QUIT_CMD which - * always set msg.obj to the handler. - */ - boolean recordLogMsg = mSm.recordLogRec(mMsg) && (msg.obj != sSmHandlerObj); - - if (mLogRecords.logOnlyTransitions()) { - /** Record only if there is a transition */ - if (mDestState != null) { - mLogRecords.add(mSm, mMsg, mSm.getLogRecString(mMsg), msgProcessedState, - orgState, mDestState); - } - } else if (recordLogMsg) { - /** Record message */ - mLogRecords.add(mSm, mMsg, mSm.getLogRecString(mMsg), msgProcessedState, orgState, - mDestState); - } - - State destState = mDestState; - if (destState != null) { - /** - * Process the transitions including transitions in the enter/exit methods - */ - while (true) { - if (mDbg) mSm.log("handleMessage: new destination call exit/enter"); - - /** - * Determine the states to exit and enter and return the - * common ancestor state of the enter/exit states. Then - * invoke the exit methods then the enter methods. - */ - StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(destState); - // flag is cleared in invokeEnterMethods before entering the target state - mTransitionInProgress = true; - invokeExitMethods(commonStateInfo); - int stateStackEnteringIndex = moveTempStateStackToStateStack(); - invokeEnterMethods(stateStackEnteringIndex); - - /** - * Since we have transitioned to a new state we need to have - * any deferred messages moved to the front of the message queue - * so they will be processed before any other messages in the - * message queue. - */ - moveDeferredMessageAtFrontOfQueue(); - - if (destState != mDestState) { - // A new mDestState so continue looping - destState = mDestState; - } else { - // No change in mDestState so we're done - break; - } - } - mDestState = null; - } - - /** - * After processing all transitions check and - * see if the last transition was to quit or halt. - */ - if (destState != null) { - if (destState == mQuittingState) { - /** - * Call onQuitting to let subclasses cleanup. - */ - mSm.onQuitting(); - cleanupAfterQuitting(); - } else if (destState == mHaltingState) { - /** - * Call onHalting() if we've transitioned to the halting - * state. All subsequent messages will be processed in - * in the halting state which invokes haltedProcessMessage(msg); - */ - mSm.onHalting(); - } - } - } - - /** - * Cleanup all the static variables and the looper after the SM has been quit. - */ - private final void cleanupAfterQuitting() { - if (mSm.mSmThread != null) { - // If we made the thread then quit looper which stops the thread. - getLooper().quit(); - mSm.mSmThread = null; - } - - mSm.mSmHandler = null; - mSm = null; - mMsg = null; - mLogRecords.cleanup(); - mStateStack = null; - mTempStateStack = null; - mStateInfo.clear(); - mInitialState = null; - mDestState = null; - mDeferredMessages.clear(); - mHasQuit = true; - } - - /** - * Complete the construction of the state machine. - */ - private final void completeConstruction() { - if (mDbg) mSm.log("completeConstruction: E"); - - /** - * Determine the maximum depth of the state hierarchy - * so we can allocate the state stacks. - */ - int maxDepth = 0; - for (StateInfo si : mStateInfo.values()) { - int depth = 0; - for (StateInfo i = si; i != null; depth++) { - i = i.parentStateInfo; - } - if (maxDepth < depth) { - maxDepth = depth; - } - } - if (mDbg) mSm.log("completeConstruction: maxDepth=" + maxDepth); - - mStateStack = new StateInfo[maxDepth]; - mTempStateStack = new StateInfo[maxDepth]; - setupInitialStateStack(); - - /** Sending SM_INIT_CMD message to invoke enter methods asynchronously */ - sendMessageAtFrontOfQueue(obtainMessage(SM_INIT_CMD, sSmHandlerObj)); - - if (mDbg) mSm.log("completeConstruction: X"); - } - - /** - * Process the message. If the current state doesn't handle - * it, call the states parent and so on. If it is never handled then - * call the state machines unhandledMessage method. - * @return the state that processed the message - */ - private final State processMsg(Message msg) { - StateInfo curStateInfo = mStateStack[mStateStackTopIndex]; - if (mDbg) { - mSm.log("processMsg: " + curStateInfo.state.getName()); - } - - if (isQuit(msg)) { - transitionTo(mQuittingState); - } else { - while (!curStateInfo.state.processMessage(msg)) { - /** - * Not processed - */ - curStateInfo = curStateInfo.parentStateInfo; - if (curStateInfo == null) { - /** - * No parents left so it's not handled - */ - mSm.unhandledMessage(msg); - break; - } - if (mDbg) { - mSm.log("processMsg: " + curStateInfo.state.getName()); - } - } - } - return (curStateInfo != null) ? curStateInfo.state : null; - } - - /** - * Call the exit method for each state from the top of stack - * up to the common ancestor state. - */ - private final void invokeExitMethods(StateInfo commonStateInfo) { - while ((mStateStackTopIndex >= 0) - && (mStateStack[mStateStackTopIndex] != commonStateInfo)) { - State curState = mStateStack[mStateStackTopIndex].state; - if (mDbg) mSm.log("invokeExitMethods: " + curState.getName()); - curState.exit(); - mStateStack[mStateStackTopIndex].active = false; - mStateStackTopIndex -= 1; - } - } - - /** - * Invoke the enter method starting at the entering index to top of state stack - */ - private final void invokeEnterMethods(int stateStackEnteringIndex) { - for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) { - if (stateStackEnteringIndex == mStateStackTopIndex) { - // Last enter state for transition - mTransitionInProgress = false; - } - if (mDbg) mSm.log("invokeEnterMethods: " + mStateStack[i].state.getName()); - mStateStack[i].state.enter(); - mStateStack[i].active = true; - } - mTransitionInProgress = false; // ensure flag set to false if no methods called - } - - /** - * Move the deferred message to the front of the message queue. - */ - private final void moveDeferredMessageAtFrontOfQueue() { - /** - * The oldest messages on the deferred list must be at - * the front of the queue so start at the back, which - * as the most resent message and end with the oldest - * messages at the front of the queue. - */ - for (int i = mDeferredMessages.size() - 1; i >= 0; i--) { - Message curMsg = mDeferredMessages.get(i); - if (mDbg) mSm.log("moveDeferredMessageAtFrontOfQueue; what=" + curMsg.what); - sendMessageAtFrontOfQueue(curMsg); - } - mDeferredMessages.clear(); - } - - /** - * Move the contents of the temporary stack to the state stack - * reversing the order of the items on the temporary stack as - * they are moved. - * - * @return index into mStateStack where entering needs to start - */ - private final int moveTempStateStackToStateStack() { - int startingIndex = mStateStackTopIndex + 1; - int i = mTempStateStackCount - 1; - int j = startingIndex; - while (i >= 0) { - if (mDbg) mSm.log("moveTempStackToStateStack: i=" + i + ",j=" + j); - mStateStack[j] = mTempStateStack[i]; - j += 1; - i -= 1; - } - - mStateStackTopIndex = j - 1; - if (mDbg) { - mSm.log("moveTempStackToStateStack: X mStateStackTop=" + mStateStackTopIndex - + ",startingIndex=" + startingIndex + ",Top=" - + mStateStack[mStateStackTopIndex].state.getName()); - } - return startingIndex; - } - - /** - * Setup the mTempStateStack with the states we are going to enter. - * - * This is found by searching up the destState's ancestors for a - * state that is already active i.e. StateInfo.active == true. - * The destStae and all of its inactive parents will be on the - * TempStateStack as the list of states to enter. - * - * @return StateInfo of the common ancestor for the destState and - * current state or null if there is no common parent. - */ - private final StateInfo setupTempStateStackWithStatesToEnter(State destState) { - /** - * Search up the parent list of the destination state for an active - * state. Use a do while() loop as the destState must always be entered - * even if it is active. This can happen if we are exiting/entering - * the current state. - */ - mTempStateStackCount = 0; - StateInfo curStateInfo = mStateInfo.get(destState); - do { - mTempStateStack[mTempStateStackCount++] = curStateInfo; - curStateInfo = curStateInfo.parentStateInfo; - } while ((curStateInfo != null) && !curStateInfo.active); - - if (mDbg) { - mSm.log("setupTempStateStackWithStatesToEnter: X mTempStateStackCount=" - + mTempStateStackCount + ",curStateInfo: " + curStateInfo); - } - return curStateInfo; - } - - /** - * Initialize StateStack to mInitialState. - */ - private final void setupInitialStateStack() { - if (mDbg) { - mSm.log("setupInitialStateStack: E mInitialState=" + mInitialState.getName()); - } - - StateInfo curStateInfo = mStateInfo.get(mInitialState); - for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) { - mTempStateStack[mTempStateStackCount] = curStateInfo; - curStateInfo = curStateInfo.parentStateInfo; - } - - // Empty the StateStack - mStateStackTopIndex = -1; - - moveTempStateStackToStateStack(); - } - - /** - * @return current message - */ - private final Message getCurrentMessage() { - return mMsg; - } - - /** - * @return current state - */ - private final IState getCurrentState() { - return mStateStack[mStateStackTopIndex].state; - } - - /** - * Add a new state to the state machine. Bottom up addition - * of states is allowed but the same state may only exist - * in one hierarchy. - * - * @param state the state to add - * @param parent the parent of state - * @return stateInfo for this state - */ - private final StateInfo addState(State state, State parent) { - if (mDbg) { - mSm.log("addStateInternal: E state=" + state.getName() + ",parent=" - + ((parent == null) ? "" : parent.getName())); - } - StateInfo parentStateInfo = null; - if (parent != null) { - parentStateInfo = mStateInfo.get(parent); - if (parentStateInfo == null) { - // Recursively add our parent as it's not been added yet. - parentStateInfo = addState(parent, null); - } - } - StateInfo stateInfo = mStateInfo.get(state); - if (stateInfo == null) { - stateInfo = new StateInfo(); - mStateInfo.put(state, stateInfo); - } - - // Validate that we aren't adding the same state in two different hierarchies. - if ((stateInfo.parentStateInfo != null) - && (stateInfo.parentStateInfo != parentStateInfo)) { - throw new RuntimeException("state already added"); - } - stateInfo.state = state; - stateInfo.parentStateInfo = parentStateInfo; - stateInfo.active = false; - if (mDbg) mSm.log("addStateInternal: X stateInfo: " + stateInfo); - return stateInfo; - } - - /** - * Remove a state from the state machine. Will not remove the state if it is currently - * active or if it has any children in the hierarchy. - * @param state the state to remove - */ - private void removeState(State state) { - StateInfo stateInfo = mStateInfo.get(state); - if (stateInfo == null || stateInfo.active) { - return; - } - boolean isParent = mStateInfo.values().stream() - .filter(si -> si.parentStateInfo == stateInfo) - .findAny() - .isPresent(); - if (isParent) { - return; - } - mStateInfo.remove(state); - } - - /** - * Constructor - * - * @param looper for dispatching messages - * @param sm the hierarchical state machine - */ - private SmHandler(Looper looper, StateMachine sm) { - super(looper); - mSm = sm; - - addState(mHaltingState, null); - addState(mQuittingState, null); - } - - /** @see StateMachine#setInitialState(State) */ - private final void setInitialState(State initialState) { - if (mDbg) mSm.log("setInitialState: initialState=" + initialState.getName()); - mInitialState = initialState; - } - - /** @see StateMachine#transitionTo(IState) */ - private final void transitionTo(IState destState) { - if (mTransitionInProgress) { - Log.wtf(mSm.mName, "transitionTo called while transition already in progress to " + - mDestState + ", new target state=" + destState); - } - mDestState = (State) destState; - if (mDbg) mSm.log("transitionTo: destState=" + mDestState.getName()); - } - - /** @see StateMachine#deferMessage(Message) */ - private final void deferMessage(Message msg) { - if (mDbg) mSm.log("deferMessage: msg=" + msg.what); - - /* Copy the "msg" to "newMsg" as "msg" will be recycled */ - Message newMsg = obtainMessage(); - newMsg.copyFrom(msg); - - mDeferredMessages.add(newMsg); - } - - /** @see StateMachine#quit() */ - private final void quit() { - if (mDbg) mSm.log("quit:"); - sendMessage(obtainMessage(SM_QUIT_CMD, sSmHandlerObj)); - } - - /** @see StateMachine#quitNow() */ - private final void quitNow() { - if (mDbg) mSm.log("quitNow:"); - sendMessageAtFrontOfQueue(obtainMessage(SM_QUIT_CMD, sSmHandlerObj)); - } - - /** Validate that the message was sent by quit or quitNow. */ - private final boolean isQuit(Message msg) { - return (msg.what == SM_QUIT_CMD) && (msg.obj == sSmHandlerObj); - } - - /** @see StateMachine#isDbg() */ - private final boolean isDbg() { - return mDbg; - } - - /** @see StateMachine#setDbg(boolean) */ - private final void setDbg(boolean dbg) { - mDbg = dbg; - } - - } - - private SmHandler mSmHandler; - private HandlerThread mSmThread; - - /** - * Initialize. - * - * @param looper for this state machine - * @param name of the state machine - */ - private void initStateMachine(String name, Looper looper) { - mName = name; - mSmHandler = new SmHandler(looper, this); - } - - /** - * Constructor creates a StateMachine with its own thread. - * - * @param name of the state machine - */ - @UnsupportedAppUsage - protected StateMachine(String name) { - mSmThread = new HandlerThread(name); - mSmThread.start(); - Looper looper = mSmThread.getLooper(); - - initStateMachine(name, looper); - } - - /** - * Constructor creates a StateMachine using the looper. - * - * @param name of the state machine - */ - @UnsupportedAppUsage - protected StateMachine(String name, Looper looper) { - initStateMachine(name, looper); - } - - /** - * Constructor creates a StateMachine using the handler. - * - * @param name of the state machine - */ - @UnsupportedAppUsage - protected StateMachine(String name, Handler handler) { - initStateMachine(name, handler.getLooper()); - } - - /** - * Notifies subclass that the StateMachine handler is about to process the Message msg - * @param msg The message that is being handled - */ - protected void onPreHandleMessage(Message msg) { - } - - /** - * Notifies subclass that the StateMachine handler has finished processing the Message msg and - * has possibly transitioned to a new state. - * @param msg The message that is being handled - */ - protected void onPostHandleMessage(Message msg) { - } - - /** - * Add a new state to the state machine - * @param state the state to add - * @param parent the parent of state - */ - public final void addState(State state, State parent) { - mSmHandler.addState(state, parent); - } - - /** - * Add a new state to the state machine, parent will be null - * @param state to add - */ - @UnsupportedAppUsage - public final void addState(State state) { - mSmHandler.addState(state, null); - } - - /** - * Removes a state from the state machine, unless it is currently active or if it has children. - * @param state state to remove - */ - public final void removeState(State state) { - mSmHandler.removeState(state); - } - - /** - * Set the initial state. This must be invoked before - * and messages are sent to the state machine. - * - * @param initialState is the state which will receive the first message. - */ - @UnsupportedAppUsage - public final void setInitialState(State initialState) { - mSmHandler.setInitialState(initialState); - } - - /** - * @return current message - */ - public final Message getCurrentMessage() { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return null; - return smh.getCurrentMessage(); - } - - /** - * @return current state - */ - public final IState getCurrentState() { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return null; - return smh.getCurrentState(); - } - - /** - * transition to destination state. Upon returning - * from processMessage the current state's exit will - * be executed and upon the next message arriving - * destState.enter will be invoked. - * - * this function can also be called inside the enter function of the - * previous transition target, but the behavior is undefined when it is - * called mid-way through a previous transition (for example, calling this - * in the enter() routine of a intermediate node when the current transition - * target is one of the nodes descendants). - * - * @param destState will be the state that receives the next message. - */ - @UnsupportedAppUsage - public final void transitionTo(IState destState) { - mSmHandler.transitionTo(destState); - } - - /** - * transition to halt state. Upon returning - * from processMessage we will exit all current - * states, execute the onHalting() method and then - * for all subsequent messages haltedProcessMessage - * will be called. - */ - public final void transitionToHaltingState() { - mSmHandler.transitionTo(mSmHandler.mHaltingState); - } - - /** - * Defer this message until next state transition. - * Upon transitioning all deferred messages will be - * placed on the queue and reprocessed in the original - * order. (i.e. The next state the oldest messages will - * be processed first) - * - * @param msg is deferred until the next transition. - */ - public final void deferMessage(Message msg) { - mSmHandler.deferMessage(msg); - } - - /** - * Called when message wasn't handled - * - * @param msg that couldn't be handled. - */ - protected void unhandledMessage(Message msg) { - if (mSmHandler.mDbg) loge(" - unhandledMessage: msg.what=" + msg.what); - } - - /** - * Called for any message that is received after - * transitionToHalting is called. - */ - protected void haltedProcessMessage(Message msg) { - } - - /** - * This will be called once after handling a message that called - * transitionToHalting. All subsequent messages will invoke - * {@link StateMachine#haltedProcessMessage(Message)} - */ - protected void onHalting() { - } - - /** - * This will be called once after a quit message that was NOT handled by - * the derived StateMachine. The StateMachine will stop and any subsequent messages will be - * ignored. In addition, if this StateMachine created the thread, the thread will - * be stopped after this method returns. - */ - protected void onQuitting() { - } - - /** - * @return the name - */ - public final String getName() { - return mName; - } - - /** - * Set number of log records to maintain and clears all current records. - * - * @param maxSize number of messages to maintain at anyone time. - */ - public final void setLogRecSize(int maxSize) { - mSmHandler.mLogRecords.setSize(maxSize); - } - - /** - * Set to log only messages that cause a state transition - * - * @param enable {@code true} to enable, {@code false} to disable - */ - public final void setLogOnlyTransitions(boolean enable) { - mSmHandler.mLogRecords.setLogOnlyTransitions(enable); - } - - /** - * @return the number of log records currently readable - */ - public final int getLogRecSize() { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return 0; - return smh.mLogRecords.size(); - } - - /** - * @return the number of log records we can store - */ - @VisibleForTesting - public final int getLogRecMaxSize() { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return 0; - return smh.mLogRecords.mMaxSize; - } - - /** - * @return the total number of records processed - */ - public final int getLogRecCount() { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return 0; - return smh.mLogRecords.count(); - } - - /** - * @return a log record, or null if index is out of range - */ - public final LogRec getLogRec(int index) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return null; - return smh.mLogRecords.get(index); - } - - /** - * @return a copy of LogRecs as a collection - */ - public final Collection<LogRec> copyLogRecs() { - Vector<LogRec> vlr = new Vector<LogRec>(); - SmHandler smh = mSmHandler; - if (smh != null) { - for (LogRec lr : smh.mLogRecords.mLogRecVector) { - vlr.add(lr); - } - } - return vlr; - } - - /** - * Add the string to LogRecords. - * - * @param string - */ - public void addLogRec(String string) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - smh.mLogRecords.add(this, smh.getCurrentMessage(), string, smh.getCurrentState(), - smh.mStateStack[smh.mStateStackTopIndex].state, smh.mDestState); - } - - /** - * @return true if msg should be saved in the log, default is true. - */ - protected boolean recordLogRec(Message msg) { - return true; - } - - /** - * Return a string to be logged by LogRec, default - * is an empty string. Override if additional information is desired. - * - * @param msg that was processed - * @return information to be logged as a String - */ - protected String getLogRecString(Message msg) { - return ""; - } - - /** - * @return the string for msg.what - */ - protected String getWhatToString(int what) { - return null; - } - - /** - * @return Handler, maybe null if state machine has quit. - */ - public final Handler getHandler() { - return mSmHandler; - } - - /** - * Get a message and set Message.target state machine handler. - * - * Note: The handler can be null if the state machine has quit, - * which means target will be null and may cause a AndroidRuntimeException - * in MessageQueue#enqueMessage if sent directly or if sent using - * StateMachine#sendMessage the message will just be ignored. - * - * @return A Message object from the global pool - */ - public final Message obtainMessage() { - return Message.obtain(mSmHandler); - } - - /** - * Get a message and set Message.target state machine handler, what. - * - * Note: The handler can be null if the state machine has quit, - * which means target will be null and may cause a AndroidRuntimeException - * in MessageQueue#enqueMessage if sent directly or if sent using - * StateMachine#sendMessage the message will just be ignored. - * - * @param what is the assigned to Message.what. - * @return A Message object from the global pool - */ - public final Message obtainMessage(int what) { - return Message.obtain(mSmHandler, what); - } - - /** - * Get a message and set Message.target state machine handler, - * what and obj. - * - * Note: The handler can be null if the state machine has quit, - * which means target will be null and may cause a AndroidRuntimeException - * in MessageQueue#enqueMessage if sent directly or if sent using - * StateMachine#sendMessage the message will just be ignored. - * - * @param what is the assigned to Message.what. - * @param obj is assigned to Message.obj. - * @return A Message object from the global pool - */ - public final Message obtainMessage(int what, Object obj) { - return Message.obtain(mSmHandler, what, obj); - } - - /** - * Get a message and set Message.target state machine handler, - * what, arg1 and arg2 - * - * Note: The handler can be null if the state machine has quit, - * which means target will be null and may cause a AndroidRuntimeException - * in MessageQueue#enqueMessage if sent directly or if sent using - * StateMachine#sendMessage the message will just be ignored. - * - * @param what is assigned to Message.what - * @param arg1 is assigned to Message.arg1 - * @return A Message object from the global pool - */ - public final Message obtainMessage(int what, int arg1) { - // use this obtain so we don't match the obtain(h, what, Object) method - return Message.obtain(mSmHandler, what, arg1, 0); - } - - /** - * Get a message and set Message.target state machine handler, - * what, arg1 and arg2 - * - * Note: The handler can be null if the state machine has quit, - * which means target will be null and may cause a AndroidRuntimeException - * in MessageQueue#enqueMessage if sent directly or if sent using - * StateMachine#sendMessage the message will just be ignored. - * - * @param what is assigned to Message.what - * @param arg1 is assigned to Message.arg1 - * @param arg2 is assigned to Message.arg2 - * @return A Message object from the global pool - */ - @UnsupportedAppUsage - public final Message obtainMessage(int what, int arg1, int arg2) { - return Message.obtain(mSmHandler, what, arg1, arg2); - } - - /** - * Get a message and set Message.target state machine handler, - * what, arg1, arg2 and obj - * - * Note: The handler can be null if the state machine has quit, - * which means target will be null and may cause a AndroidRuntimeException - * in MessageQueue#enqueMessage if sent directly or if sent using - * StateMachine#sendMessage the message will just be ignored. - * - * @param what is assigned to Message.what - * @param arg1 is assigned to Message.arg1 - * @param arg2 is assigned to Message.arg2 - * @param obj is assigned to Message.obj - * @return A Message object from the global pool - */ - @UnsupportedAppUsage - public final Message obtainMessage(int what, int arg1, int arg2, Object obj) { - return Message.obtain(mSmHandler, what, arg1, arg2, obj); - } - - /** - * Enqueue a message to this state machine. - * - * Message is ignored if state machine has quit. - */ - @UnsupportedAppUsage - public void sendMessage(int what) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessage(obtainMessage(what)); - } - - /** - * Enqueue a message to this state machine. - * - * Message is ignored if state machine has quit. - */ - @UnsupportedAppUsage - public void sendMessage(int what, Object obj) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessage(obtainMessage(what, obj)); - } - - /** - * Enqueue a message to this state machine. - * - * Message is ignored if state machine has quit. - */ - @UnsupportedAppUsage - public void sendMessage(int what, int arg1) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessage(obtainMessage(what, arg1)); - } - - /** - * Enqueue a message to this state machine. - * - * Message is ignored if state machine has quit. - */ - public void sendMessage(int what, int arg1, int arg2) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessage(obtainMessage(what, arg1, arg2)); - } - - /** - * Enqueue a message to this state machine. - * - * Message is ignored if state machine has quit. - */ - @UnsupportedAppUsage - public void sendMessage(int what, int arg1, int arg2, Object obj) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessage(obtainMessage(what, arg1, arg2, obj)); - } - - /** - * Enqueue a message to this state machine. - * - * Message is ignored if state machine has quit. - */ - @UnsupportedAppUsage - public void sendMessage(Message msg) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessage(msg); - } - - /** - * Enqueue a message to this state machine after a delay. - * - * Message is ignored if state machine has quit. - */ - public void sendMessageDelayed(int what, long delayMillis) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageDelayed(obtainMessage(what), delayMillis); - } - - /** - * Enqueue a message to this state machine after a delay. - * - * Message is ignored if state machine has quit. - */ - public void sendMessageDelayed(int what, Object obj, long delayMillis) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageDelayed(obtainMessage(what, obj), delayMillis); - } - - /** - * Enqueue a message to this state machine after a delay. - * - * Message is ignored if state machine has quit. - */ - public void sendMessageDelayed(int what, int arg1, long delayMillis) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageDelayed(obtainMessage(what, arg1), delayMillis); - } - - /** - * Enqueue a message to this state machine after a delay. - * - * Message is ignored if state machine has quit. - */ - public void sendMessageDelayed(int what, int arg1, int arg2, long delayMillis) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageDelayed(obtainMessage(what, arg1, arg2), delayMillis); - } - - /** - * Enqueue a message to this state machine after a delay. - * - * Message is ignored if state machine has quit. - */ - public void sendMessageDelayed(int what, int arg1, int arg2, Object obj, - long delayMillis) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageDelayed(obtainMessage(what, arg1, arg2, obj), delayMillis); - } - - /** - * Enqueue a message to this state machine after a delay. - * - * Message is ignored if state machine has quit. - */ - public void sendMessageDelayed(Message msg, long delayMillis) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageDelayed(msg, delayMillis); - } - - /** - * Enqueue a message to the front of the queue for this state machine. - * Protected, may only be called by instances of StateMachine. - * - * Message is ignored if state machine has quit. - */ - protected final void sendMessageAtFrontOfQueue(int what) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageAtFrontOfQueue(obtainMessage(what)); - } - - /** - * Enqueue a message to the front of the queue for this state machine. - * Protected, may only be called by instances of StateMachine. - * - * Message is ignored if state machine has quit. - */ - protected final void sendMessageAtFrontOfQueue(int what, Object obj) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageAtFrontOfQueue(obtainMessage(what, obj)); - } - - /** - * Enqueue a message to the front of the queue for this state machine. - * Protected, may only be called by instances of StateMachine. - * - * Message is ignored if state machine has quit. - */ - protected final void sendMessageAtFrontOfQueue(int what, int arg1) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1)); - } - - - /** - * Enqueue a message to the front of the queue for this state machine. - * Protected, may only be called by instances of StateMachine. - * - * Message is ignored if state machine has quit. - */ - protected final void sendMessageAtFrontOfQueue(int what, int arg1, int arg2) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1, arg2)); - } - - /** - * Enqueue a message to the front of the queue for this state machine. - * Protected, may only be called by instances of StateMachine. - * - * Message is ignored if state machine has quit. - */ - protected final void sendMessageAtFrontOfQueue(int what, int arg1, int arg2, Object obj) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageAtFrontOfQueue(obtainMessage(what, arg1, arg2, obj)); - } - - /** - * Enqueue a message to the front of the queue for this state machine. - * Protected, may only be called by instances of StateMachine. - * - * Message is ignored if state machine has quit. - */ - protected final void sendMessageAtFrontOfQueue(Message msg) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.sendMessageAtFrontOfQueue(msg); - } - - /** - * Removes a message from the message queue. - * Protected, may only be called by instances of StateMachine. - */ - protected final void removeMessages(int what) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.removeMessages(what); - } - - /** - * Removes a message from the deferred messages queue. - */ - protected final void removeDeferredMessages(int what) { - SmHandler smh = mSmHandler; - if (smh == null) return; - - Iterator<Message> iterator = smh.mDeferredMessages.iterator(); - while (iterator.hasNext()) { - Message msg = iterator.next(); - if (msg.what == what) iterator.remove(); - } - } - - /** - * Check if there are any pending messages with code 'what' in deferred messages queue. - */ - protected final boolean hasDeferredMessages(int what) { - SmHandler smh = mSmHandler; - if (smh == null) return false; - - Iterator<Message> iterator = smh.mDeferredMessages.iterator(); - while (iterator.hasNext()) { - Message msg = iterator.next(); - if (msg.what == what) return true; - } - - return false; - } - - /** - * Check if there are any pending posts of messages with code 'what' in - * the message queue. This does NOT check messages in deferred message queue. - */ - protected final boolean hasMessages(int what) { - SmHandler smh = mSmHandler; - if (smh == null) return false; - - return smh.hasMessages(what); - } - - /** - * Validate that the message was sent by - * {@link StateMachine#quit} or {@link StateMachine#quitNow}. - * */ - protected final boolean isQuit(Message msg) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return msg.what == SM_QUIT_CMD; - - return smh.isQuit(msg); - } - - /** - * Quit the state machine after all currently queued up messages are processed. - */ - public final void quit() { - // mSmHandler can be null if the state machine is already stopped. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.quit(); - } - - /** - * Quit the state machine immediately all currently queued messages will be discarded. - */ - public final void quitNow() { - // mSmHandler can be null if the state machine is already stopped. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.quitNow(); - } - - /** - * @return if debugging is enabled - */ - public boolean isDbg() { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return false; - - return smh.isDbg(); - } - - /** - * Set debug enable/disabled. - * - * @param dbg is true to enable debugging. - */ - public void setDbg(boolean dbg) { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - smh.setDbg(dbg); - } - - /** - * Start the state machine. - */ - @UnsupportedAppUsage - public void start() { - // mSmHandler can be null if the state machine has quit. - SmHandler smh = mSmHandler; - if (smh == null) return; - - /** Send the complete construction message */ - smh.completeConstruction(); - } - - /** - * Dump the current state. - * - * @param fd - * @param pw - * @param args - */ - @UnsupportedAppUsage - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println(getName() + ":"); - pw.println(" total records=" + getLogRecCount()); - for (int i = 0; i < getLogRecSize(); i++) { - pw.println(" rec[" + i + "]: " + getLogRec(i).toString()); - pw.flush(); - } - pw.println("curState=" + getCurrentState().getName()); - } - - @Override - public String toString() { - String name = "(null)"; - String state = "(null)"; - try { - name = mName.toString(); - state = mSmHandler.getCurrentState().getName().toString(); - } catch (NullPointerException | ArrayIndexOutOfBoundsException e) { - // Will use default(s) initialized above. - } - return "name=" + name + " state=" + state; - } - - /** - * Log with debug and add to the LogRecords. - * - * @param s is string log - */ - protected void logAndAddLogRec(String s) { - addLogRec(s); - log(s); - } - - /** - * Log with debug - * - * @param s is string log - */ - protected void log(String s) { - Log.d(mName, s); - } - - /** - * Log with debug attribute - * - * @param s is string log - */ - protected void logd(String s) { - Log.d(mName, s); - } - - /** - * Log with verbose attribute - * - * @param s is string log - */ - protected void logv(String s) { - Log.v(mName, s); - } - - /** - * Log with info attribute - * - * @param s is string log - */ - protected void logi(String s) { - Log.i(mName, s); - } - - /** - * Log with warning attribute - * - * @param s is string log - */ - protected void logw(String s) { - Log.w(mName, s); - } - - /** - * Log with error attribute - * - * @param s is string log - */ - protected void loge(String s) { - Log.e(mName, s); - } - - /** - * Log with error attribute - * - * @param s is string log - * @param e is a Throwable which logs additional information. - */ - protected void loge(String s, Throwable e) { - Log.e(mName, s, e); - } -} diff --git a/src/com/android/cellbroadcastreceiver/WakeLockStateMachine.java b/src/com/android/cellbroadcastreceiver/WakeLockStateMachine.java deleted file mode 100644 index 4de06f4f5..000000000 --- a/src/com/android/cellbroadcastreceiver/WakeLockStateMachine.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright (C) 2013 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.cellbroadcastreceiver; - -import android.annotation.UnsupportedAppUsage; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.Message; -import android.os.PowerManager; - -import android.util.Log; -import com.android.internal.util.State; -import com.android.internal.util.StateMachine; - -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Generic state machine for handling messages and waiting for ordered broadcasts to complete. - * Subclasses implement {@link #handleSmsMessage}, which returns true to transition into waiting - * state, or false to remain in idle state. The wakelock is acquired on exit from idle state, - * and is released a few seconds after returning to idle state, or immediately upon calling - * {@link #quit}. - */ -public abstract class WakeLockStateMachine extends StateMachine { - protected static final boolean DBG = Build.IS_DEBUGGABLE; - - private final PowerManager.WakeLock mWakeLock; - - /** New message to process. */ - public static final int EVENT_NEW_SMS_MESSAGE = 1; - - /** Result receiver called for current cell broadcast. */ - protected static final int EVENT_BROADCAST_COMPLETE = 2; - - /** Release wakelock after a short timeout when returning to idle state. */ - static final int EVENT_RELEASE_WAKE_LOCK = 3; - - @UnsupportedAppUsage - protected Context mContext; - - protected AtomicInteger mReceiverCount = new AtomicInteger(0); - - /** Wakelock release delay when returning to idle state. */ - private static final int WAKE_LOCK_TIMEOUT = 3000; - - private final DefaultState mDefaultState = new DefaultState(); - @UnsupportedAppUsage - private final IdleState mIdleState = new IdleState(); - private final WaitingState mWaitingState = new WaitingState(); - - protected WakeLockStateMachine(String debugTag, Context context) { - super(debugTag); - - mContext = context; - - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, debugTag); - // wake lock released after we enter idle state - mWakeLock.acquire(); - - addState(mDefaultState); - addState(mIdleState, mDefaultState); - addState(mWaitingState, mDefaultState); - setInitialState(mIdleState); - } - - private void releaseWakeLock() { - if (mWakeLock.isHeld()) { - mWakeLock.release(); - } - - if (mWakeLock.isHeld()) { - loge("Wait lock is held after release."); - } - } - - /** - * Tell the state machine to quit after processing all messages. - */ - public final void dispose() { - quit(); - } - - @Override - protected void onQuitting() { - // fully release the wakelock - while (mWakeLock.isHeld()) { - mWakeLock.release(); - } - } - - /** - * Send a message with the specified object for {@link #handleSmsMessage}. - * @param obj the object to pass in the msg.obj field - */ - public final void dispatchSmsMessage(Object obj) { - sendMessage(EVENT_NEW_SMS_MESSAGE, obj); - } - - /** - * This parent state throws an exception (for debug builds) or prints an error for unhandled - * message types. - */ - class DefaultState extends State { - @Override - public boolean processMessage(Message msg) { - switch (msg.what) { - default: { - String errorText = "processMessage: unhandled message type " + msg.what; - if (Build.IS_DEBUGGABLE) { - throw new RuntimeException(errorText); - } else { - loge(errorText); - } - break; - } - } - return HANDLED; - } - } - - /** - * Idle state delivers Cell Broadcasts to receivers. It acquires the wakelock, which is - * released when the broadcast completes. - */ - class IdleState extends State { - @Override - public void enter() { - sendMessageDelayed(EVENT_RELEASE_WAKE_LOCK, WAKE_LOCK_TIMEOUT); - } - - @Override - public void exit() { - mWakeLock.acquire(); - if (DBG) log("acquired wakelock, leaving Idle state"); - } - - @Override - public boolean processMessage(Message msg) { - switch (msg.what) { - case EVENT_NEW_SMS_MESSAGE: - // transition to waiting state if we sent a broadcast - if (handleSmsMessage(msg)) { - transitionTo(mWaitingState); - } - return HANDLED; - - case EVENT_RELEASE_WAKE_LOCK: - releaseWakeLock(); - return HANDLED; - - default: - return NOT_HANDLED; - } - } - } - - /** - * Waiting state waits for the result receiver to be called for the current cell broadcast. - * In this state, any new cell broadcasts are deferred until we return to Idle state. - */ - class WaitingState extends State { - @Override - public boolean processMessage(Message msg) { - switch (msg.what) { - case EVENT_NEW_SMS_MESSAGE: - log("deferring message until return to idle"); - deferMessage(msg); - return HANDLED; - - case EVENT_BROADCAST_COMPLETE: - log("broadcast complete, returning to idle"); - transitionTo(mIdleState); - return HANDLED; - - case EVENT_RELEASE_WAKE_LOCK: - releaseWakeLock(); - return HANDLED; - - default: - return NOT_HANDLED; - } - } - } - - /** - * Implemented by subclass to handle messages in {@link IdleState}. - * @param message the message to process - * @return true to transition to {@link WaitingState}; false to stay in {@link IdleState} - */ - protected abstract boolean handleSmsMessage(Message message); - - /** - * BroadcastReceiver to send message to return to idle state. - */ - protected final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (mReceiverCount.decrementAndGet() == 0) { - sendMessage(EVENT_BROADCAST_COMPLETE); - } - } - }; - - /** - * Log with debug level. - * @param s the string to log - */ - @UnsupportedAppUsage - @Override - protected void log(String s) { - Log.d(getName(), s); - } - - /** - * Log with error level. - * @param s the string to log - */ - @Override - protected void loge(String s) { - Log.e(getName(), s); - } - - /** - * Log with error level. - * @param s the string to log - * @param e is a Throwable which logs additional information. - */ - @Override - protected void loge(String s, Throwable e) { - Log.e(getName(), s, e); - } -} diff --git a/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendCdmaCmasMessages.java b/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendCdmaCmasMessages.java index 24ab11b7e..0103068a6 100644 --- a/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendCdmaCmasMessages.java +++ b/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendCdmaCmasMessages.java @@ -132,7 +132,7 @@ public class SendCdmaCmasMessages { return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP2, SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, serialNumber, new SmsCbLocation("123456"), serviceCategory, language, body, - SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, null, cmasInfo); + SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, null, cmasInfo, 0 /*subId*/); } /** diff --git a/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendGsmCmasMessages.java b/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendGsmCmasMessages.java index c9082a97d..5a3d0b993 100644 --- a/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendGsmCmasMessages.java +++ b/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendGsmCmasMessages.java @@ -218,7 +218,7 @@ public class SendGsmCmasMessages { certainty); return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, 0, serialNumber, new SmsCbLocation("123456"), serviceCategory, language, body, - priority, null, cmasInfo); + priority, null, cmasInfo, 0/*slotIndex*/); } /** diff --git a/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendTestMessages.java b/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendTestMessages.java index fef8f5d94..9cee85d31 100644 --- a/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendTestMessages.java +++ b/tests/testapp/src/com/android/cellbroadcastreceiver/tests/SendTestMessages.java @@ -423,7 +423,7 @@ public class SendTestMessages { } } return GsmSmsCbMessage.createSmsCbMessage(context, new SmsCbHeader(pdus[0]), - sEmptyLocation, pdus); + sEmptyLocation, pdus, 0 /* slotIndex */); } catch (IllegalArgumentException e) { return null; } diff --git a/tests/unit/AndroidTest.xml b/tests/unit/AndroidTest.xml index 328fecb1f..8e676981b 100644 --- a/tests/unit/AndroidTest.xml +++ b/tests/unit/AndroidTest.xml @@ -15,7 +15,7 @@ --> <configuration description="Run CellBroadcastReceiver Unit Test Cases."> <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> - <option name="test-file-name" value="CellBroadcastReceiverPlatformUnitTests.apk" /> + <option name="test-file-name" value="CellBroadcastReceiverUnitTests.apk" /> </target_preparer> <option name="test-suite-tag" value="apct" /> diff --git a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialogTest.java b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialogTest.java index 29a28b2c0..db8c4c938 100644 --- a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialogTest.java +++ b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialogTest.java @@ -26,7 +26,6 @@ import android.content.Intent; import android.os.Bundle; import android.os.IPowerManager; import android.os.PowerManager; -import android.telephony.CellBroadcastMessage; import android.widget.TextView; import org.junit.After; diff --git a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastAlertServiceTest.java b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastAlertServiceTest.java index 727df077c..86096c9fa 100644 --- a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastAlertServiceTest.java +++ b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastAlertServiceTest.java @@ -27,7 +27,6 @@ import android.content.Intent; import android.os.PersistableBundle; import android.provider.Telephony; import android.telephony.CarrierConfigManager; -import android.telephony.CellBroadcastMessage; import android.telephony.SmsCbCmasInfo; import android.telephony.SmsCbEtwsInfo; import android.telephony.SmsCbLocation; @@ -50,7 +49,8 @@ public class CellBroadcastAlertServiceTest extends static SmsCbMessage createMessage(int serialNumber) { return new SmsCbMessage(1, 2, serialNumber, new SmsCbLocation(), SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL, "language", "body", - SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, null, new SmsCbCmasInfo(0, 2, 3, 4, 5, 6)); + SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, null, new SmsCbCmasInfo(0, 2, 3, 4, 5, 6), + 0 /*subId*/); } @Before @@ -89,10 +89,9 @@ public class CellBroadcastAlertServiceTest extends compareEtwsWarningInfo(cbm1.getEtwsWarningInfo(), cbm2.getEtwsWarningInfo()); assertEquals(cbm1.getLanguageCode(), cbm2.getLanguageCode()); assertEquals(cbm1.getMessageBody(), cbm2.getMessageBody()); - assertEquals(cbm1.getSerialNumber(), cbm2.getSerialNumber()); assertEquals(cbm1.getServiceCategory(), cbm2.getServiceCategory()); - assertEquals(cbm1.getSubId(), cbm2.getSubId()); - assertEquals(cbm1.getSerialNumber(), cbm2.getSerialNumber()); + assertEquals(cbm1.getSmsCbMessage().getSerialNumber(), + cbm2.getSmsCbMessage().getSerialNumber()); } private void sendMessage(int serialNumber) { @@ -232,7 +231,7 @@ public class CellBroadcastAlertServiceTest extends CellBroadcastMessage cbmTest = (CellBroadcastMessage) mServiceIntentToVerify.getExtras().get("message"); - assertEquals(91924, cbmTest.getSerialNumber()); + assertEquals(91924, cbmTest.getSmsCbMessage().getSerialNumber()); mServiceIntentToVerify = null; // Wait until it expires. @@ -242,7 +241,7 @@ public class CellBroadcastAlertServiceTest extends // Since the previous one has already expired, this one should not be treated as a duplicate cbmTest = (CellBroadcastMessage) mServiceIntentToVerify.getExtras().get("message"); - assertEquals(91924, cbmTest.getSerialNumber()); + assertEquals(91924, cbmTest.getSmsCbMessage().getSerialNumber()); waitForMs(500); mServiceIntentToVerify = null; diff --git a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastChannelManagerTest.java b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastChannelManagerTest.java index 42402482b..f89853198 100644 --- a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastChannelManagerTest.java +++ b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastChannelManagerTest.java @@ -18,6 +18,7 @@ package com.android.cellbroadcastreceiver; import static org.junit.Assert.assertEquals; +import android.telephony.SubscriptionManager; import android.test.suitebuilder.annotation.SmallTest; import com.android.cellbroadcastreceiver.CellBroadcastAlertService.AlertType; @@ -59,9 +60,11 @@ public class CellBroadcastChannelManagerTest extends CellBroadcastTest { "0xA804:type=test, emergency=true" }); - ArrayList<CellBroadcastChannelRange> list = - CellBroadcastChannelManager.getCellBroadcastChannelRanges( - mContext, R.array.additional_cbs_channels_strings); + CellBroadcastChannelManager channelManager = new CellBroadcastChannelManager(mContext, + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); + + ArrayList<CellBroadcastChannelRange> list = channelManager.getCellBroadcastChannelRanges( + R.array.additional_cbs_channels_strings); assertEquals(12, list.get(0).mStartId); assertEquals(12, list.get(0).mEndId); diff --git a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastConfigServiceTest.java b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastConfigServiceTest.java index 186bfeb14..c89d84873 100644 --- a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastConfigServiceTest.java +++ b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastConfigServiceTest.java @@ -29,6 +29,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.SharedPreferences; import android.telephony.SmsManager; +import android.telephony.SubscriptionManager; import android.test.suitebuilder.annotation.SmallTest; import com.android.cellbroadcastreceiver.CellBroadcastChannelManager.CellBroadcastChannelRange; @@ -59,8 +60,6 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { private CellBroadcastConfigService mConfigService; - private SmsManager mSmsManager = SmsManager.getDefault(); - @Before public void setUp() throws Exception { super.setUp(getClass().getSimpleName()); @@ -126,11 +125,12 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { super.tearDown(); } - private void setCellBroadcastRange(boolean enable, List<CellBroadcastChannelRange> ranges) + private void setCellBroadcastRange(int subId, boolean enable, + List<CellBroadcastChannelRange> ranges) throws Exception { Class[] cArgs = new Class[3]; - cArgs[0] = SmsManager.class; + cArgs[0] = Integer.TYPE; cArgs[1] = Boolean.TYPE; cArgs[2] = List.class; @@ -138,7 +138,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { CellBroadcastConfigService.class.getDeclaredMethod("setCellBroadcastRange", cArgs); method.setAccessible(true); - method.invoke(mConfigService, mSmsManager, enable, ranges); + method.invoke(mConfigService, subId, enable, ranges); } /** @@ -148,8 +148,9 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { @SmallTest public void testEnableCellBroadcastRange() throws Exception { ArrayList<CellBroadcastChannelRange> result = new ArrayList<>(); - result.add(new CellBroadcastChannelRange(mContext, "10-20")); - setCellBroadcastRange(true, result); + result.add(new CellBroadcastChannelRange(mContext, + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "10-20")); + setCellBroadcastRange(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, true, result); ArgumentCaptor<Integer> captorStart = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> captorEnd = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> captorType = ArgumentCaptor.forClass(Integer.class); @@ -169,8 +170,9 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { @SmallTest public void testDisableCellBroadcastRange() throws Exception { ArrayList<CellBroadcastChannelRange> result = new ArrayList<>(); - result.add(new CellBroadcastChannelRange(mContext, "10-20")); - setCellBroadcastRange(false, result); + result.add(new CellBroadcastChannelRange(mContext, + SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, "10-20")); + setCellBroadcastRange(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, false, result); ArgumentCaptor<Integer> captorStart = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> captorEnd = ArgumentCaptor.forClass(Integer.class); ArgumentCaptor<Integer> captorType = ArgumentCaptor.forClass(Integer.class); @@ -198,7 +200,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).enableCellBroadcastRangeForSubscriber( eq(0), @@ -288,7 +290,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { @SmallTest public void testEnablingPresidential() throws Exception { setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).enableCellBroadcastRangeForSubscriber( eq(0), @@ -309,7 +311,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, false); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), @@ -330,21 +332,21 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, false); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); - verify(mMockedSmsService, times(1)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(3)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT), eq(SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_CDMA)); - verify(mMockedSmsService, times(1)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(3)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); - verify(mMockedSmsService, times(1)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(3)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL_LANGUAGE), @@ -360,7 +362,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { public void testEnablingExtreme() throws Exception { setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).enableCellBroadcastRangeForSubscriber( eq(0), @@ -381,7 +383,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, false); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).disableCellBroadcastRangeForSubscriber( eq(0), @@ -402,21 +404,21 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, false); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT), eq(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_CDMA)); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED_LANGUAGE), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY_LANGUAGE), @@ -432,7 +434,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { public void testEnablingSevere() throws Exception { setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).enableCellBroadcastRangeForSubscriber( eq(0), @@ -453,7 +455,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, false); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).disableCellBroadcastRangeForSubscriber( eq(0), @@ -474,21 +476,21 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, false); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT), eq(SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_CDMA)); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED_LANGUAGE), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY_LANGUAGE), @@ -503,7 +505,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { public void testEnablingAmber() throws Exception { setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).enableCellBroadcastRangeForSubscriber( eq(0), @@ -524,7 +526,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, false); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).disableCellBroadcastRangeForSubscriber( eq(0), @@ -545,21 +547,21 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, false); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY), eq(SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_CDMA)); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE), eq(SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY_LANGUAGE), @@ -573,7 +575,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { @SmallTest public void testEnablingETWS() throws Exception { setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).enableCellBroadcastRangeForSubscriber( eq(0), @@ -588,7 +590,7 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, false); - mConfigService.setCellBroadcastOnSub(mSmsManager, true); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); verify(mMockedSmsService, times(1)).disableCellBroadcastRangeForSubscriber( eq(0), @@ -603,15 +605,15 @@ public class CellBroadcastConfigServiceTest extends CellBroadcastTest { eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); setPreference(CellBroadcastSettings.KEY_ENABLE_ALERTS_MASTER_TOGGLE, true); - mConfigService.setCellBroadcastOnSub(mSmsManager, false); + mConfigService.enableCellBroadcastChannels(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING), eq(SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_AND_TSUNAMI_WARNING), eq(SmsManager.CELL_BROADCAST_RAN_TYPE_GSM)); - verify(mMockedSmsService, times(2)).disableCellBroadcastRangeForSubscriber( + verify(mMockedSmsService, times(2)).enableCellBroadcastRangeForSubscriber( eq(0), eq(SmsCbConstants.MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE), eq(SmsCbConstants.MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE), diff --git a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastServiceTestCase.java b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastServiceTestCase.java index c29dff499..158c84f0f 100644 --- a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastServiceTestCase.java +++ b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastServiceTestCase.java @@ -17,6 +17,7 @@ package com.android.cellbroadcastreceiver; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doReturn; @@ -108,10 +109,13 @@ public abstract class CellBroadcastServiceTestCase<T extends Service> extends Se public void setUp() throws Exception { MockitoAnnotations.initMocks(this); // A hack to return mResources from static method - // CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context). + // CellBroadcastSettings.getResources(context). doReturn(mSubService).when(mSubService).queryLocalInterface(anyString()); doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID).when(mSubService).getDefaultSubId(); doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID).when(mSubService).getDefaultSmsSubId(); + + doReturn(new String[]{""}).when(mResources).getStringArray(anyInt()); + mMockedServiceManager = new MockedServiceManager(); mMockedServiceManager.replaceService("isub", mSubService); mContext = new TestContextWrapper(getContext()); diff --git a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastTest.java b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastTest.java index c97e6382f..da8e2f3a4 100644 --- a/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastTest.java +++ b/tests/unit/src/com/android/cellbroadcastreceiver/CellBroadcastTest.java @@ -16,6 +16,7 @@ package com.android.cellbroadcastreceiver; +import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; @@ -54,7 +55,7 @@ public abstract class CellBroadcastTest { TAG = tag; MockitoAnnotations.initMocks(this); // A hack to return mResources from static method - // CellBroadcastSettings.getResourcesForDefaultSmsSubscriptionId(context). + // CellBroadcastSettings.getResources(context). doReturn(mSubService).when(mSubService).queryLocalInterface(anyString()); doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID).when(mSubService).getDefaultSubId(); doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID).when(mSubService).getDefaultSmsSubId(); @@ -67,6 +68,8 @@ public abstract class CellBroadcastTest { doReturn(mCarrierConfigManager).when(mContext) .getSystemService(eq(Context.CARRIER_CONFIG_SERVICE)); doReturn(mResources).when(mContext).getResources(); + doReturn(mContext).when(mContext).getApplicationContext(); + doReturn(new String[]{""}).when(mResources).getStringArray(anyInt()); } void carrierConfigSetStringArray(int subId, String key, String[] values) { |
