diff options
author | Jake Hamby <jhamby@google.com> | 2012-02-13 18:34:01 -0800 |
---|---|---|
committer | Jake Hamby <jhamby@google.com> | 2012-03-13 16:48:21 -0700 |
commit | 00b87064abfb9d254fbbf72110643d2e626365e6 (patch) | |
tree | e5e540eec2dd2990d407bd7f2f6d443a619f58d2 | |
parent | 95903862da74128bbab5c81d782c5ff277172498 (diff) | |
download | CellBroadcastReceiver-00b87064abfb9d254fbbf72110643d2e626365e6.tar.gz |
Add CMAS over CDMA support to CellBroadcastReceiver app.
Add support for CMAS warning notifications over CDMA to
CellBroadcastReceiver app. The framework code now sends cell
broadcasts in a radio-technology independent form by passing the new
Parcelable android.telephony.SmsCbMessage class as the "message"
intent of the SMS_CB_RECEIVED_ACTION or SMS_EMERGENCY_CB_RECEIVED_ACTION,
instead of as a GSM/UMTS "pdu" byte array.
The database version has also been upgraded to encompass metadata for
ETWS and CMAS emergency broadcasts, as well as CDMA format broadcasts.
Bug: 5856308
Change-Id: I1b10d35d8aa6f0134260116980e4c326f86d2fe7
36 files changed, 2640 insertions, 872 deletions
diff --git a/Android.mk b/Android.mk index 5343d1aef..f439ec4ef 100644 --- a/Android.mk +++ b/Android.mk @@ -8,6 +8,7 @@ LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under, src) LOCAL_PACKAGE_NAME := CellBroadcastReceiver +LOCAL_CERTIFICATE := platform include $(BUILD_PACKAGE) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 59234db81..e9c915872 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -54,12 +54,31 @@ <category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> + <intent-filter> + <action android:name="android.cellbroadcastreceiver.UPDATE_LIST_VIEW" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> </activity> <activity android:name="CellBroadcastSettings" android:label="@string/sms_cb_settings" android:exported="true" /> + <activity android:name="CellBroadcastAlertDialog" + android:excludeFromRecents="true" + android:theme="@android:style/Theme.Holo.Dialog" + android:launchMode="singleInstance" + android:exported="false" + android:configChanges="orientation|keyboardHidden|keyboard|navigation" /> + + <!-- Full-screen version of CellBroadcastAlertDialog to display alerts over lock screen. --> + <activity android:name="CellBroadcastAlertFullScreen" + android:excludeFromRecents="true" + android:theme="@style/AlertFullScreenTheme" + android:launchMode="singleInstance" + android:exported="false" + android:configChanges="orientation|keyboardHidden|keyboard|navigation" /> + <!-- Require sender permissions to prevent SMS spoofing --> <receiver android:name="PrivilegedCellBroadcastReceiver" android:permission="android.permission.BROADCAST_SMS"> @@ -70,6 +89,10 @@ <intent-filter> <action android:name="android.provider.Telephony.SMS_CB_RECEIVED" /> </intent-filter> + + <intent-filter> + <action android:name="android.provider.Telephony.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED" /> + </intent-filter> </receiver> <receiver android:name="CellBroadcastReceiver"> diff --git a/res/drawable-hdpi/alert_fullscreen_bg.9.png b/res/drawable-hdpi/alert_fullscreen_bg.9.png Binary files differnew file mode 100644 index 000000000..79e56f522 --- /dev/null +++ b/res/drawable-hdpi/alert_fullscreen_bg.9.png diff --git a/res/drawable-mdpi/alert_fullscreen_bg.9.png b/res/drawable-mdpi/alert_fullscreen_bg.9.png Binary files differnew file mode 100644 index 000000000..fb3660eab --- /dev/null +++ b/res/drawable-mdpi/alert_fullscreen_bg.9.png diff --git a/res/drawable-xhdpi/alert_fullscreen_bg.9.png b/res/drawable-xhdpi/alert_fullscreen_bg.9.png Binary files differnew file mode 100644 index 000000000..f4970ad1c --- /dev/null +++ b/res/drawable-xhdpi/alert_fullscreen_bg.9.png diff --git a/res/layout-sw600dp/cell_broadcast_alert_fullscreen.xml b/res/layout-sw600dp/cell_broadcast_alert_fullscreen.xml new file mode 100644 index 000000000..0430edacc --- /dev/null +++ b/res/layout-sw600dp/cell_broadcast_alert_fullscreen.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" > + + <FrameLayout + android:layout_width="400dp" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@drawable/alert_fullscreen_bg" > + <include layout="@layout/cell_broadcast_alert" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </FrameLayout> + +</FrameLayout> diff --git a/res/layout/cell_broadcast_alert.xml b/res/layout/cell_broadcast_alert.xml new file mode 100644 index 000000000..85b129fb7 --- /dev/null +++ b/res/layout/cell_broadcast_alert.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 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. +*/ +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/parentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="8dip" + android:layout_marginRight="8dip" + android:orientation="vertical"> + + <LinearLayout android:id="@+id/topPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + <View android:id="@+id/titleDividerTop" + android:layout_width="match_parent" + android:layout_height="2dip" + android:visibility="gone" + android:background="@android:color/holo_blue_light" /> + <LinearLayout android:id="@+id/title_template" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical|left" + android:minHeight="@dimen/alert_dialog_title_height" + android:layout_marginLeft="16dip" + android:layout_marginRight="16dip"> + <ImageView android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingRight="8dip" + android:src="@null" /> + <com.android.internal.widget.DialogTitle android:id="@+id/alertTitle" + style="?android:attr/windowTitleStyle" + android:singleLine="true" + android:ellipsize="end" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </LinearLayout> + <View android:id="@+id/titleDivider" + android:layout_width="match_parent" + android:layout_height="2dip" + android:visibility="gone" + android:background="@android:color/holo_blue_light" /> + </LinearLayout> + + <LinearLayout android:id="@+id/contentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical"> + <ScrollView android:id="@+id/scrollView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false"> + <TextView android:id="@+id/message" + style="?android:attr/textAppearanceMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="16dip" + android:paddingRight="16dip" + android:paddingTop="8dip" + android:paddingBottom="8dip"/> + </ScrollView> + </LinearLayout> + + <LinearLayout android:id="@+id/buttonPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/alert_dialog_button_bar_height" + android:orientation="vertical" + android:divider="?android:attr/dividerHorizontal" + android:showDividers="beginning" + android:dividerPadding="0dip"> + <LinearLayout + style="?android:attr/buttonBarStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layoutDirection="locale" + android:measureWithLargestChild="true"> + <Button android:id="@+id/dismissButton" + android:layout_width="0dip" + android:layout_gravity="center_horizontal" + android:layout_weight="1" + android:maxLines="2" + style="?android:attr/buttonBarButtonStyle" + android:text="@string/button_dismiss" + android:textSize="14sp" + android:minHeight="@dimen/alert_dialog_button_bar_height" + android:layout_height="wrap_content" /> + </LinearLayout> + </LinearLayout> +</LinearLayout> diff --git a/res/layout/cell_broadcast_alert_fullscreen.xml b/res/layout/cell_broadcast_alert_fullscreen.xml new file mode 100644 index 000000000..c73a495eb --- /dev/null +++ b/res/layout/cell_broadcast_alert_fullscreen.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginLeft="10dp" + android:layout_marginRight="10dp" > + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@drawable/alert_fullscreen_bg" > + <include layout="@layout/cell_broadcast_alert" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </FrameLayout> + +</FrameLayout> diff --git a/res/layout/cell_broadcast_list_screen.xml b/res/layout/cell_broadcast_list_screen.xml index 44de8ee92..1e9631514 100644 --- a/res/layout/cell_broadcast_list_screen.xml +++ b/res/layout/cell_broadcast_list_screen.xml @@ -16,13 +16,25 @@ * limitations under the License. */ --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/listContainer" + android:layout_width="match_parent" + android:layout_height="match_parent"> -<ListView android:id="@android:id/list" xmlns:android="http://schemas.android.com/apk/res/android" - style="?android:attr/listViewWhiteStyle" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:drawSelectorOnTop="false" - android:scrollbarStyle="insideOverlay" - android:background="@android:color/white" - android:cacheColorHint="@android:color/white" - android:fadingEdgeLength="16dip" /> + <ListView android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:drawSelectorOnTop="false" + android:scrollbarStyle="insideOverlay" + android:background="@android:color/white" + android:cacheColorHint="@android:color/white" + android:fadingEdgeLength="16dip" /> + + <TextView android:id="@+android:id/empty" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:text="@string/no_cell_broadcasts" + android:textAppearance="?android:attr/textAppearanceLarge" /> + +</FrameLayout> diff --git a/res/values/dimens.xml b/res/values/dimens.xml new file mode 100644 index 000000000..b87330761 --- /dev/null +++ b/res/values/dimens.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** +** Copyright 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. +*/ +--> +<resources> + + <!-- Dialog title height --> + <dimen name="alert_dialog_title_height">64dip</dimen> + <!-- Dialog button bar height --> + <dimen name="alert_dialog_button_bar_height">48dip</dimen> + +</resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index e28365ae0..420bf91c0 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -26,10 +26,16 @@ <!-- Text for dismiss button in broadcast message view dialog. [CHAR LIMIT=25] --> <string name="button_dismiss">OK</string> + <!-- Text for list view when empty (no broadcasts). [CHAR LIMIT=200] --> + <string name="no_cell_broadcasts">There are no active alerts in your area. You can change the Alert settings using the Settings menu option.</string> + <!-- Menu item for accessing application settings. [CHAR LIMIT=30] --> <string name="menu_preferences">Settings</string> <!-- Menu item for deleting all broadcasts. [CHAR LIMIT=30] --> <string name="menu_delete_all">Delete broadcasts</string> + + <!-- Header for context menu on an individual broadcast. [CHAR LIMIT=30] --> + <string name="message_options">Message options</string> <!-- Context menu item to view a previously received broadcast. [CHAR LIMIT=30] --> <string name="menu_view">View broadcast</string> <!-- Context menu item to delete a previously received broadcast. [CHAR LIMIT=30] --> @@ -94,24 +100,27 @@ <!-- Preference category title for ETWS settings. [CHAR LIMIT=50] --> <string name="category_etws_settings_title">ETWS settings</string> <!-- Preference title for enable ETWS test alerts checkbox. [CHAR LIMIT=30] --> - <string name="enable_etws_test_alerts_title">Show test broadcasts</string> + <string name="enable_etws_test_alerts_title">Show ETWS test broadcasts</string> <!-- Preference summary for enable ETWS test alerts checkbox. [CHAR LIMIT=100] --> <string name="enable_etws_test_alerts_summary">Display test broadcasts for Earthquake Tsunami Warning System</string> - <!-- Preference category title for CMAS settings. [CHAR LIMIT=50] --> - <string name="category_cmas_settings_title">CMAS settings</string> - <!-- Preference title for enable CMAS imminent threat alerts checkbox. [CHAR LIMIT=30] --> - <string name="enable_cmas_imminent_threat_alerts_title">Show imminent threats</string> - <!-- Preference summary for enable CMAS imminent threat alerts checkbox. [CHAR LIMIT=100] --> - <string name="enable_cmas_imminent_threat_alerts_summary">Display CMAS imminent threat warning notifications</string> + <!-- Preference title for enable CMAS extreme threat alerts checkbox. [CHAR LIMIT=30] --> + <string name="enable_cmas_extreme_threat_alerts_title">Show extreme threats</string> + <!-- Preference summary for enable CMAS extreme threat alerts checkbox. [CHAR LIMIT=100] --> + <string name="enable_cmas_extreme_threat_alerts_summary">Display alerts for extreme threats to life and property</string> + <!-- Preference title for enable CMAS severe threat alerts checkbox. [CHAR LIMIT=30] --> + <string name="enable_cmas_severe_threat_alerts_title">Show severe threats</string> + <!-- Preference summary for enable CMAS severe threat alerts checkbox. [CHAR LIMIT=100] --> + <string name="enable_cmas_severe_threat_alerts_summary">Display alerts for severe threats to life and property</string> <!-- Preference title for enable CMAS amber alerts checkbox. [CHAR LIMIT=50] --> - <string name="enable_cmas_amber_alerts_title">Show Amber alerts</string> + <string name="enable_cmas_amber_alerts_title">Show AMBER alerts</string> <!-- Preference summary for enable CMAS amber alerts checkbox. [CHAR LIMIT=100] --> - <string name="enable_cmas_amber_alerts_summary">Display Amber alert (child abduction emergency) bulletins</string> + <string name="enable_cmas_amber_alerts_summary">Display child abduction emergency bulletins (AMBER alert)</string> + <!-- Preference title for enable CMAS test alerts checkbox. [CHAR LIMIT=30] --> - <string name="enable_cmas_test_alerts_title">Show test broadcasts</string> + <string name="enable_cmas_test_alerts_title">Show CMAS test broadcasts</string> <!-- Preference summary for enable CMAS test alerts checkbox. [CHAR LIMIT=100] --> - <string name="enable_cmas_test_alerts_summary">Display CMAS monthly test broadcasts</string> + <string name="enable_cmas_test_alerts_summary">Display test broadcasts for Commercial Mobile Alert System</string> <!-- Preference category title for Brazil settings. [CHAR LIMIT=50] --> <string name="category_brazil_settings_title">Settings for Brazil</string> @@ -120,6 +129,81 @@ <!-- Preference summary for enable channel 50 alerts (Brazil only). [CHAR LIMIT=100] --> <string name="enable_channel_50_alerts_summary">Channel 50 is used in Brazil for area update information</string> + <!-- Preference category title for developer settings. [CHAR LIMIT=50] --> + <string name="category_dev_settings_title">Developer options</string> + + <!-- CMAS alert category heading (including colon and space). [CHAR LIMIT=30] --> + <string name="cmas_category_heading">Category: </string> + <!-- CMAS category for geophysical alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_geo">Geophysical</string> + <!-- CMAS category for meteorological alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_met">Meteorological</string> + <!-- CMAS category for general emergency and public safety alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_safety">Safety</string> + <!-- CMAS category for security alerts (law enforcement, military, etc.). [CHAR LIMIT=50] --> + <string name="cmas_category_security">Security</string> + <!-- CMAS category for rescue and recovery alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_rescue">Rescue</string> + <!-- CMAS category for fire suppression and rescue alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_fire">Fire</string> + <!-- CMAS category for medical and public health alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_health">Health</string> + <!-- CMAS category for pollution and other environmental alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_env">Environmental</string> + <!-- CMAS category for transportation alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_transport">Transportation</string> + <!-- CMAS category for utility, telecommunication, and other infrastructure alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_infra">Infrastructure</string> + <!-- CMAS category for chemical, biological, radiological, nuclear alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_cbrne">Chemical/Biological/Nuclear/Explosive</string> + <!-- CMAS category for other alerts. [CHAR LIMIT=50] --> + <string name="cmas_category_other">Other</string> + + <!-- CMAS response type heading (including colon and space). [CHAR LIMIT=30] --> + <string name="cmas_response_heading">Response type: </string> + <!-- CMAS response type: take shelter in place. [CHAR LIMIT=50] --> + <string name="cmas_response_shelter">Take shelter</string> + <!-- CMAS response type: evacuate (relocate). [CHAR LIMIT=50] --> + <string name="cmas_response_evacuate">Evacuate</string> + <!-- CMAS response type: make preparations. [CHAR LIMIT=50] --> + <string name="cmas_response_prepare">Prepare</string> + <!-- CMAS response type: execute a pre-planned activity. [CHAR LIMIT=50] --> + <string name="cmas_response_execute">Execute activity</string> + <!-- CMAS response type: monitor information sources. [CHAR LIMIT=50] --> + <string name="cmas_response_monitor">Monitor info sources</string> + <!-- CMAS response type: avoid hazard. [CHAR LIMIT=50] --> + <string name="cmas_response_avoid">Avoid hazard</string> + <!-- CMAS response type: evaluate the information in this message. [CHAR LIMIT=50] --> + <string name="cmas_response_assess">Evaluate</string> + <!-- CMAS response type: no action recommended. [CHAR LIMIT=50] --> + <string name="cmas_response_none">No action</string> + + <!-- CMAS severity heading (including colon and space). [CHAR LIMIT=30] --> + <string name="cmas_severity_heading">Severity: </string> + <!-- CMAS severity type: extreme. [CHAR LIMIT=30] --> + <string name="cmas_severity_extreme">Extreme</string> + <!-- CMAS severity type: severe. [CHAR LIMIT=30] --> + <string name="cmas_severity_severe">Severe</string> + + <!-- CMAS urgency heading (including colon and space). [CHAR LIMIT=30] --> + <string name="cmas_urgency_heading">Urgency: </string> + <!-- CMAS urgency type: take responsive action immediately. [CHAR LIMIT=30] --> + <string name="cmas_urgency_immediate">Take action immediately</string> + <!-- CMAS severity type: severe. [CHAR LIMIT=30] --> + <string name="cmas_urgency_expected">Take action within the next hour</string> + + <!-- CMAS certainty heading (including colon and space). [CHAR LIMIT=30] --> + <string name="cmas_certainty_heading">Certainty: </string> + <!-- CMAS certainty type: observed. [CHAR LIMIT=30] --> + <string name="cmas_certainty_observed">Observed</string> + <!-- CMAS severity type: severe. [CHAR LIMIT=30] --> + <string name="cmas_certainty_likely">Likely</string> + + <!-- Non-emergency broadcast notification description for multiple unread alerts. --> + <string name="notification_multiple"><xliff:g id="count">%s</xliff:g> unread alerts.</string> + <!-- Non-emergency broadcast notification title for multiple unread alerts. --> + <string name="notification_multiple_title">New alerts</string> + <!-- Entries listed in the ListPreference for allowed alert durations. [CHAR LIMIT=30] --> <string-array name="alert_sound_duration_entries"> <item>2 seconds</item> diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 000000000..2cdbd2db3 --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** +** Copyright 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. +*/ +--> +<resources> + + <!-- This must be a fullscreen theme for the alarm to be able to turn the screen on. --> + <style name="AlertFullScreenTheme" parent="android:Theme.Holo.Wallpaper.NoTitleBar" /> + +</resources> diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index e1a5bc9c8..164bbd0d5 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -18,55 +18,30 @@ --> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Emergency alert settings for CMAS. --> <PreferenceCategory android:title="@string/emergency_alert_settings_title" - android:key="emergency_alert_settings"> + android:key="category_cmas_settings"> + <!-- Enable CMAS Extreme Threat alerts --> <CheckBoxPreference android:defaultValue="true" - android:key="enable_emergency_alerts" - android:summary="@string/enable_emergency_alerts_summary" - android:title="@string/enable_emergency_alerts_title" /> - - <ListPreference android:key="alert_sound_duration" - android:title="@string/alert_sound_duration_title" - android:entries="@array/alert_sound_duration_entries" - android:entryValues="@array/alert_sound_duration_values" - android:defaultValue="4" - android:dialogTitle="@string/alert_sound_duration_title" /> + android:key="enable_cmas_extreme_threat_alerts" + android:summary="@string/enable_cmas_extreme_threat_alerts_summary" + android:title="@string/enable_cmas_extreme_threat_alerts_title" /> + <!-- Enable CMAS Severe Threat alerts --> <CheckBoxPreference android:defaultValue="true" - android:key="enable_alert_speech" - android:summary="@string/enable_alert_speech_summary" - android:title="@string/enable_alert_speech_title" /> - - </PreferenceCategory> - <PreferenceCategory android:title="@string/category_etws_settings_title" - android:key="category_etws_settings"> - - <CheckBoxPreference android:defaultValue="false" - android:key="enable_etws_test_alerts" - android:summary="@string/enable_etws_test_alerts_summary" - android:title="@string/enable_etws_test_alerts_title" /> - - </PreferenceCategory> - <PreferenceCategory android:title="@string/category_cmas_settings_title" - android:key="category_cmas_settings"> + android:key="enable_cmas_severe_threat_alerts" + android:summary="@string/enable_cmas_severe_threat_alerts_summary" + android:title="@string/enable_cmas_severe_threat_alerts_title" /> + <!-- Enable CMAS AMBER alerts --> <CheckBoxPreference android:defaultValue="true" - android:key="enable_cmas_imminent_threat_alerts" - android:summary="@string/enable_cmas_imminent_threat_alerts_summary" - android:title="@string/enable_cmas_imminent_threat_alerts_title" /> - - <CheckBoxPreference android:defaultValue="false" android:key="enable_cmas_amber_alerts" android:summary="@string/enable_cmas_amber_alerts_summary" android:title="@string/enable_cmas_amber_alerts_title" /> - <CheckBoxPreference android:defaultValue="false" - android:key="enable_cmas_test_alerts" - android:summary="@string/enable_cmas_test_alerts_summary" - android:title="@string/enable_cmas_test_alerts_title" /> - </PreferenceCategory> + <PreferenceCategory android:title="@string/category_brazil_settings_title" android:key="category_brazil_settings"> @@ -79,4 +54,37 @@ </PreferenceCategory> + <!-- Only visible when Developer options toggle is enabled in Settings. --> + <PreferenceCategory android:title="@string/category_dev_settings_title" + android:key="category_dev_settings"> + + <CheckBoxPreference android:defaultValue="false" + android:key="enable_etws_test_alerts" + android:summary="@string/enable_etws_test_alerts_summary" + android:title="@string/enable_etws_test_alerts_title" /> + + <CheckBoxPreference android:defaultValue="false" + android:key="enable_cmas_test_alerts" + android:summary="@string/enable_cmas_test_alerts_summary" + android:title="@string/enable_cmas_test_alerts_title" /> + + <CheckBoxPreference android:defaultValue="true" + android:key="enable_emergency_alerts" + android:summary="@string/enable_emergency_alerts_summary" + android:title="@string/enable_emergency_alerts_title" /> + + <ListPreference android:key="alert_sound_duration" + android:title="@string/alert_sound_duration_title" + android:entries="@array/alert_sound_duration_entries" + android:entryValues="@array/alert_sound_duration_values" + android:defaultValue="4" + android:dialogTitle="@string/alert_sound_duration_title" /> + + <CheckBoxPreference android:defaultValue="true" + android:key="enable_alert_speech" + android:summary="@string/enable_alert_speech_summary" + android:title="@string/enable_alert_speech_title" /> + + </PreferenceCategory> + </PreferenceScreen> diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java index 1ae70c133..ac5055d41 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertAudio.java @@ -27,6 +27,7 @@ import android.media.MediaPlayer.OnErrorListener; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.os.PowerManager; import android.os.Vibrator; import android.speech.tts.TextToSpeech; import android.telephony.PhoneStateListener; @@ -66,6 +67,9 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI /** Vibration uses the same on/off pattern as the CMAS alert tone */ private static final long[] sVibratePattern = new long[] { 0, 2000, 500, 1000, 500, 1000, 500 }; + /** CPU wake lock while playing audio. */ + private PowerManager.WakeLock mWakeLock; + private static final int STATE_IDLE = 0; private static final int STATE_ALERTING = 1; private static final int STATE_PAUSING = 2; @@ -141,6 +145,7 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI * Callback from TTS engine after initialization. * @param status {@link TextToSpeech#SUCCESS} or {@link TextToSpeech#ERROR}. */ + @Override public void onInit(int status) { if (DBG) Log.v(TAG, "onInit() TTS engine status: " + status); if (status == TextToSpeech.SUCCESS) { @@ -176,12 +181,18 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI * Callback from TTS engine. * @param utteranceId the identifier of the utterance. */ + @Override public void onUtteranceCompleted(String utteranceId) { stopSelf(); } @Override public void onCreate() { + // acquire CPU wake lock while playing audio + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); + mWakeLock.acquire(); + mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); // Listen for incoming calls to kill the alarm. @@ -189,7 +200,6 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); mTelephonyManager.listen( mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - CellBroadcastAlertWakeLock.acquireCpuWakeLock(this); } @Override @@ -197,12 +207,13 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI stop(); // Stop listening for incoming calls. mTelephonyManager.listen(mPhoneStateListener, 0); - CellBroadcastAlertWakeLock.releaseCpuLock(); // shutdown TTS engine if (mTts != null) { mTts.stop(); mTts.shutdown(); } + // release CPU wake lock + mWakeLock.release(); } @Override @@ -276,6 +287,7 @@ public class CellBroadcastAlertAudio extends Service implements TextToSpeech.OnI Log.v(TAG, "in call: reducing volume"); mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); } + // start playing alert audio setDataSourceFromResource(getResources(), mMediaPlayer, R.raw.attention_signal); mAudioManager.requestAudioFocus(null, AudioManager.STREAM_ALARM, diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java index eed3d20a7..e762678a1 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertDialog.java @@ -16,168 +16,71 @@ package com.android.cellbroadcastreceiver; -import android.app.AlertDialog; import android.app.KeyguardManager; +import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; -import android.graphics.drawable.Drawable; +import android.content.IntentFilter; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.view.WindowManager; -import android.widget.ImageView; +import android.util.Log; /** * Custom alert dialog with optional flashing warning icon. * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}. + * Keyguard handling based on {@code AlarmAlert} class from DeskClock app. */ -public class CellBroadcastAlertDialog extends AlertDialog { +public class CellBroadcastAlertDialog extends CellBroadcastAlertFullScreen { + private static final String TAG = "CellBroadcastAlertDialog"; - /** Whether to show the flashing warning icon. */ - private final boolean mShowWarningIcon; + private BroadcastReceiver mScreenOffReceiver; - /** The broadcast delivery time, for marking as read (or 0). */ - private final long mDeliveryTime; - - /** Length of time for the warning icon to be visible. */ - private static final int WARNING_ICON_ON_DURATION_MSEC = 800; - - /** Length of time for the warning icon to be off. */ - private static final int WARNING_ICON_OFF_DURATION_MSEC = 800; - - /** Warning icon state. false = visible, true = off */ - private boolean mIconAnimationState; - - /** Stop animating icon after {@link #onStop()} is called. */ - private boolean mStopAnimation; - - /** The warning icon Drawable. */ - private Drawable mWarningIcon; - - /** The View containing the warning icon. */ - private ImageView mWarningIconView; - - /** Keyguard lock to show emergency alerts while in the lock screen. */ - private KeyguardManager.KeyguardLock mKeyguardLock; - - /** Icon animation handler for flashing warning alerts. */ - private final Handler mAnimationHandler = new Handler() { + private class ScreenOffReceiver extends BroadcastReceiver { @Override - public void handleMessage(Message msg) { - if (mIconAnimationState) { - mWarningIconView.setAlpha(255); - if (!mStopAnimation) { - mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_ON_DURATION_MSEC); - } - } else { - mWarningIconView.setAlpha(0); - if (!mStopAnimation) { - mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_OFF_DURATION_MSEC); - } - } - mIconAnimationState = !mIconAnimationState; - mWarningIconView.invalidateDrawable(mWarningIcon); - } - }; - - /** - * Create a new alert dialog for the broadcast notification. - * @param context the local Context - * @param titleId the resource ID of the dialog title - * @param body the message body contents - * @param showWarningIcon true if the flashing warning icon should be shown - * @param deliveryTime the delivery time of the broadcast, for marking as read - */ - public CellBroadcastAlertDialog(Context context, int titleId, CharSequence body, - boolean showWarningIcon, long deliveryTime) { - super(context); - mShowWarningIcon = showWarningIcon; - mDeliveryTime = deliveryTime; - - setTitle(titleId); - setMessage(body); - setCancelable(true); - setOnCancelListener(new AlertDialog.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - dialog.dismiss(); - } - }); - setButton(DialogInterface.BUTTON_NEUTRAL, context.getText(R.string.button_dismiss), - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - dialog.dismiss(); - } - }); - - // Set warning icon for emergency alert - if (mShowWarningIcon) { - mWarningIcon = getContext().getResources().getDrawable(R.drawable.ic_warning_large); - setIcon(mWarningIcon); + public void onReceive(Context context, Intent intent) { + handleScreenOff(); } } @Override - protected void onCreate(Bundle savedInstanceState) { - if (mShowWarningIcon) { - // Turn screen on and show above the keyguard for emergency alert - getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD - | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON - | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON); - } - super.onCreate(savedInstanceState); - if (mShowWarningIcon) { - KeyguardManager km = (KeyguardManager) - getContext().getSystemService(Context.KEYGUARD_SERVICE); - mKeyguardLock = km.newKeyguardLock("CellBroadcastReceiver"); - mWarningIconView = (ImageView) findViewById(com.android.internal.R.id.icon); + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // Listen for the screen turning off so that when the screen comes back + // on, the user does not need to unlock the phone to dismiss the alert. + if (mMessage.isEmergencyAlertMessage()) { + mScreenOffReceiver = new ScreenOffReceiver(); + registerReceiver(mScreenOffReceiver, + new IntentFilter(Intent.ACTION_SCREEN_OFF)); } } - /** - * Start animating warning icon. - */ @Override - protected void onStart() { - if (mShowWarningIcon) { - // Disable keyguard - mKeyguardLock.disableKeyguard(); - // start icon animation - mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_ON_DURATION_MSEC); + public void onDestroy() { + super.onDestroy(); + if (mScreenOffReceiver != null) { + unregisterReceiver(mScreenOffReceiver); } } - /** - * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio} - * service if necessary. - */ @Override - protected void onStop() { - // Stop playing alert sound/vibration/speech (if started) - Context context = getContext(); - context.stopService(new Intent(context, CellBroadcastAlertAudio.class)); - // Start database service to mark broadcast as read - Intent intent = new Intent(context, CellBroadcastDatabaseService.class); - intent.setAction(CellBroadcastDatabaseService.ACTION_MARK_BROADCAST_READ); - intent.putExtra(CellBroadcastDatabaseService.DATABASE_DELIVERY_TIME_EXTRA, mDeliveryTime); - context.startService(intent); - if (mShowWarningIcon) { - // Reenable keyguard - mKeyguardLock.reenableKeyguard(); - // stop animating icon - mStopAnimation = true; - } + public void onBackPressed() { + // stop animating warning icon, stop playing alert sound, mark broadcast as read + dismiss(); } - /** - * Ignore the back button for emergency alerts (user must dismiss with button). - */ @Override - public void onBackPressed() { - if (!mShowWarningIcon) { - super.onBackPressed(); - } + protected int getLayoutResId() { + return R.layout.cell_broadcast_alert; + } + + private void handleScreenOff() { + // Launch the full screen activity but do not turn the screen on. + Intent i = new Intent(this, CellBroadcastAlertFullScreen.class); + i.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, mMessage); + i.putExtra(SCREEN_OFF, true); + startActivity(i); + finish(); } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertFullScreen.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertFullScreen.java new file mode 100644 index 000000000..ea6c5fcb7 --- /dev/null +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertFullScreen.java @@ -0,0 +1,205 @@ +/* + * 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 android.app.Activity; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.view.LayoutInflater; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * Full-screen emergency alert with flashing warning icon. + * Alert audio and text-to-speech handled by {@link CellBroadcastAlertAudio}. + * Keyguard handling based on {@code AlarmAlertFullScreen} class from DeskClock app. + */ +public class CellBroadcastAlertFullScreen extends Activity { + + /** + * Intent extra for full screen alert launched from dialog subclass as a result of the + * screen turning off. + */ + static final String SCREEN_OFF = "screen_off"; + + /** Whether to show the flashing warning icon. */ + private boolean mIsEmergencyAlert; + + /** The cell broadcast message to display. */ + CellBroadcastMessage mMessage; + + /** Length of time for the warning icon to be visible. */ + private static final int WARNING_ICON_ON_DURATION_MSEC = 800; + + /** Length of time for the warning icon to be off. */ + private static final int WARNING_ICON_OFF_DURATION_MSEC = 800; + + /** Warning icon state. false = visible, true = off */ + private boolean mIconAnimationState; + + /** Stop animating icon after {@link #onStop()} is called. */ + private boolean mStopAnimation; + + /** The warning icon Drawable. */ + private Drawable mWarningIcon; + + /** The View containing the warning icon. */ + private ImageView mWarningIconView; + + /** Icon animation handler for flashing warning alerts. */ + private final Handler mAnimationHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (mIconAnimationState) { + mWarningIconView.setAlpha(255); + if (!mStopAnimation) { + mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_ON_DURATION_MSEC); + } + } else { + mWarningIconView.setAlpha(0); + if (!mStopAnimation) { + mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_OFF_DURATION_MSEC); + } + } + mIconAnimationState = !mIconAnimationState; + mWarningIconView.invalidateDrawable(mWarningIcon); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Window win = getWindow(); + + // We use a custom title, so remove the standard dialog title bar + win.requestFeature(Window.FEATURE_NO_TITLE); + + // Full screen alerts display above the keyguard and when device is locked. + win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); + + // Turn on the screen unless we're being launched from the dialog subclass as a result of + // the screen turning off. + if (!getIntent().getBooleanExtra(SCREEN_OFF, false)) { + win.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + // Save message for passing from dialog to fullscreen activity, and for marking read. + mMessage = getIntent().getParcelableExtra( + CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); + + updateLayout(mMessage); + } + + protected int getLayoutResId() { + return R.layout.cell_broadcast_alert_fullscreen; + } + + private void updateLayout(CellBroadcastMessage message) { + LayoutInflater inflater = LayoutInflater.from(this); + + setContentView(inflater.inflate(getLayoutResId(), null)); + + /* Initialize dialog text from alert message. */ + int titleId = message.getDialogTitleResource(); + setTitle(titleId); + ((TextView) findViewById(R.id.alertTitle)).setText(titleId); + ((TextView) findViewById(R.id.message)).setText(message.getFormattedMessageBody(this)); + + /* dismiss button: close notification */ + findViewById(R.id.dismissButton).setOnClickListener( + new Button.OnClickListener() { + public void onClick(View v) { + dismiss(); + } + }); + + mIsEmergencyAlert = message.isPublicAlertMessage() || CellBroadcastConfigService + .isOperatorDefinedEmergencyId(message.getServiceCategory()); + + if (mIsEmergencyAlert) { + mWarningIcon = getResources().getDrawable(R.drawable.ic_warning_large); + mWarningIconView = (ImageView) findViewById(R.id.icon); + if (mWarningIconView != null) { + mWarningIconView.setImageDrawable(mWarningIcon); + } + + // Dismiss the notification that brought us here + ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)) + .cancel((int) message.getDeliveryTime()); + } + } + + /** + * Start animating warning icon. + */ + @Override + protected void onStart() { + super.onStart(); + if (mIsEmergencyAlert) { + // start icon animation + mAnimationHandler.sendEmptyMessageDelayed(0, WARNING_ICON_ON_DURATION_MSEC); + } + } + + /** + * Stop animating warning icon and stop the {@link CellBroadcastAlertAudio} + * service if necessary. + */ + void dismiss() { + // Stop playing alert sound/vibration/speech (if started) + stopService(new Intent(this, CellBroadcastAlertAudio.class)); + + // Start database service to mark broadcast as read + Intent intent = new Intent(this, CellBroadcastDatabaseService.class); + intent.setAction(CellBroadcastDatabaseService.ACTION_MARK_BROADCAST_READ); + // Select by delivery time because we don't know the database row ID. + intent.putExtra(CellBroadcastDatabaseService.DATABASE_DELIVERY_TIME_EXTRA, + mMessage.getDeliveryTime()); + startService(intent); + + if (mIsEmergencyAlert) { + // stop animating emergency alert icon + mStopAnimation = true; + } else { + // decrement unread non-emergency alert count + CellBroadcastReceiverApp.decrementUnreadAlertCount(); + } + finish(); + } + + /** + * Ignore the back button for emergency alerts (overridden by alert dialog so that the dialog + * is dismissed). + */ + @Override + public void onBackPressed() { + // ignored + } +} diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java index dafb3917a..a81eb32ca 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertService.java @@ -16,6 +16,7 @@ package com.android.cellbroadcastreceiver; +import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; @@ -25,9 +26,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.os.IBinder; +import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.Telephony; -import android.telephony.SmsCbConstants; +import android.telephony.SmsCbCmasInfo; import android.telephony.SmsCbMessage; import android.util.Log; @@ -44,6 +46,18 @@ public class CellBroadcastAlertService extends Service { public static final String SMS_CB_NOTIFICATION_ID_EXTRA = "com.android.cellbroadcastreceiver.SMS_CB_NOTIFICATION_ID"; + /** Intent extra to indicate a previously unread alert. */ + static final String NEW_ALERT_EXTRA = "com.android.cellbroadcastreceiver.NEW_ALERT"; + + /** Use the same notification ID for non-emergency alerts. */ + static final int NOTIFICATION_ID = 1; + + /** CPU wake lock while handling emergency alert notification. */ + private PowerManager.WakeLock mWakeLock; + + /** Hold the wake lock for 5 seconds, which should be enough time to display the alert. */ + private static final int WAKE_LOCK_TIMEOUT = 5000; + @Override public int onStartCommand(Intent intent, int flags, int startId) { String action = intent.getAction(); @@ -64,68 +78,29 @@ public class CellBroadcastAlertService extends Service { return; } - Object[] pdus = (Object[]) extras.get("pdus"); - - if (pdus == null || pdus.length < 1) { - Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no pdus"); - return; - } + SmsCbMessage message = (SmsCbMessage) extras.get("message"); - // create message from first PDU - SmsCbMessage message = SmsCbMessage.createFromPdu((byte[]) pdus[0]); if (message == null) { - Log.e(TAG, "failed to create SmsCbMessage from PDU: " + pdus[0]); + Log.e(TAG, "received SMS_CB_RECEIVED_ACTION with no message extra"); return; } - // append message bodies from any additional PDUs (GSM only) - for (int i = 1; i < pdus.length; i++) { - SmsCbMessage nextPage = SmsCbMessage.createFromPdu((byte[]) pdus[i]); - if (nextPage != null) { - message.appendToBody(nextPage.getMessageBody()); - } else { - Log.w(TAG, "failed to append to SmsCbMessage from PDU: " + pdus[i]); - // continue so we can show the first page of the broadcast - } - } - final CellBroadcastMessage cbm = new CellBroadcastMessage(message); if (!isMessageEnabledByUser(cbm)) { - Log.d(TAG, "ignoring alert of type " + cbm.getMessageIdentifier() + + Log.d(TAG, "ignoring alert of type " + cbm.getServiceCategory() + " by user preference"); return; } - // add notification to the bar - addToNotificationBar(cbm); if (cbm.isEmergencyAlertMessage() || CellBroadcastConfigService - .isOperatorDefinedEmergencyId(cbm.getMessageIdentifier())) { - // start audio/vibration/speech service for emergency alerts - Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class); - audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - String duration = prefs.getString(CellBroadcastSettings.KEY_ALERT_SOUND_DURATION, - CellBroadcastSettings.ALERT_SOUND_DEFAULT_DURATION); - audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION_EXTRA, - Integer.parseInt(duration)); - - if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) { - audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, - cbm.getMessageBody()); - - String language = cbm.getLanguageCode(); - if (cbm.isEtwsMessage() && !"ja".equals(language)) { - Log.w(TAG, "bad language code for ETWS - using Japanese TTS"); - language = "ja"; - } else if (cbm.isCmasMessage() && !"en".equals(language)) { - Log.w(TAG, "bad language code for CMAS - using English TTS"); - language = "en"; - } - audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE, - language); - } - startService(audioIntent); + .isOperatorDefinedEmergencyId(cbm.getServiceCategory())) { + // start alert sound / vibration / TTS and display full-screen alert + openEmergencyAlertNotification(cbm); + } else { + // add notification to the bar + addToNotificationBar(cbm); } + // write to database on a separate service thread Intent dbWriteIntent = new Intent(this, CellBroadcastDatabaseService.class); dbWriteIntent.setAction(CellBroadcastDatabaseService.ACTION_INSERT_NEW_BROADCAST); @@ -146,61 +121,155 @@ public class CellBroadcastAlertService extends Service { * @return true if the user has enabled this message type; false otherwise */ private boolean isMessageEnabledByUser(CellBroadcastMessage message) { - switch (message.getMessageIdentifier()) { - case SmsCbConstants.MESSAGE_ID_ETWS_TEST_MESSAGE: - return PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false); - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: - return PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_IMMINENT_THREAT_ALERTS, - true); - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY: - return PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, false); - - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST: - return PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS, false); - - default: - return true; + if (message.isEtwsTestMessage()) { + return PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(CellBroadcastSettings.KEY_ENABLE_ETWS_TEST_ALERTS, false); + } + + if (message.isCmasMessage()) { + switch (message.getCmasMessageClass()) { + case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT: + return PreferenceManager.getDefaultSharedPreferences(this).getBoolean( + CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, true); + + case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT: + return PreferenceManager.getDefaultSharedPreferences(this).getBoolean( + CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, true); + + case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY: + return PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, true); + + case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST: + case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE: + return PreferenceManager.getDefaultSharedPreferences(this) + .getBoolean(CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS, false); + + default: + return true; // presidential-level CMAS alerts are always enabled + } } + + return true; // other broadcast messages are always enabled } - private void addToNotificationBar(CellBroadcastMessage message) { + private void acquireTimedWakelock(int timeout) { + if (mWakeLock == null) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + // Note: acquiring a PARTIAL_WAKE_LOCK and setting window flag FLAG_TURN_SCREEN_ON in + // CellBroadcastAlertFullScreen is not sufficient to turn on the screen by itself. + // Use SCREEN_BRIGHT_WAKE_LOCK here as a workaround to ensure the screen turns on. + mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK + | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG); + } + mWakeLock.acquire(timeout); + } + + /** + * Display a full-screen alert message for emergency alerts. + * @param message the alert to display + */ + private void openEmergencyAlertNotification(CellBroadcastMessage message) { + // Acquire a CPU wake lock until the alert dialog and audio start playing. + acquireTimedWakelock(WAKE_LOCK_TIMEOUT); + + // Close dialogs and window shade + Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + sendBroadcast(closeDialogs); + + // start audio/vibration/speech service for emergency alerts + Intent audioIntent = new Intent(this, CellBroadcastAlertAudio.class); + audioIntent.setAction(CellBroadcastAlertAudio.ACTION_START_ALERT_AUDIO); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + String duration = prefs.getString(CellBroadcastSettings.KEY_ALERT_SOUND_DURATION, + CellBroadcastSettings.ALERT_SOUND_DEFAULT_DURATION); + audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_DURATION_EXTRA, + Integer.parseInt(duration)); + int channelTitleId = message.getDialogTitleResource(); CharSequence channelName = getText(channelTitleId); String messageBody = message.getMessageBody(); - Notification notification = new Notification(R.drawable.stat_color_warning, - channelName, System.currentTimeMillis()); + if (prefs.getBoolean(CellBroadcastSettings.KEY_ENABLE_ALERT_SPEECH, true)) { + audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_BODY, messageBody); + + String language = message.getLanguageCode(); + if (message.isEtwsMessage() && !"ja".equals(language)) { + Log.w(TAG, "bad language code for ETWS - using Japanese TTS"); + language = "ja"; + } else if (message.isCmasMessage() && !"en".equals(language)) { + Log.w(TAG, "bad language code for CMAS - using English TTS"); + language = "en"; + } + audioIntent.putExtra(CellBroadcastAlertAudio.ALERT_AUDIO_MESSAGE_LANGUAGE, + language); + } + startService(audioIntent); + + // Use lower 32 bits of emergency alert delivery time for notification ID + int notificationId = (int) message.getDeliveryTime(); + + // Decide which activity to start based on the state of the keyguard. + Class c = CellBroadcastAlertDialog.class; + KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + if (km.inKeyguardRestrictedInputMode()) { + // Use the full screen activity for security. + c = CellBroadcastAlertFullScreen.class; + } - int notificationId = CellBroadcastReceiverApp.getCellBroadcastReceiverApp() - .getNextNotificationId(); + Intent notify = createDisplayMessageIntent(this, c, message, notificationId); + PendingIntent pi = PendingIntent.getActivity(this, notificationId, notify, 0); - PendingIntent pi = PendingIntent.getActivity(this, 0, createDisplayMessageIntent( - this, message, notificationId), 0); + Notification.Builder builder = new Notification.Builder(this) + .setSmallIcon(R.drawable.stat_color_warning) + .setTicker(getText(message.getDialogTitleResource())) + .setWhen(System.currentTimeMillis()) + .setContentIntent(pi) + .setFullScreenIntent(pi, true) + .setContentTitle(channelName) + .setContentText(messageBody) + .setDefaults(Notification.DEFAULT_LIGHTS); + + NotificationManager notificationManager = + (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.notify(notificationId, builder.getNotification()); + } + + /** + * Add the new alert to the notification bar (non-emergency alerts), or launch a + * high-priority immediate intent for emergency alerts. + * @param message the alert to display + */ + private void addToNotificationBar(CellBroadcastMessage message) { + int channelTitleId = message.getDialogTitleResource(); + CharSequence channelName = getText(channelTitleId); + String messageBody = message.getMessageBody(); - notification.setLatestEventInfo(this, channelName, messageBody, pi); + // Use the same ID to create a single notification for multiple non-emergency alerts. + int notificationId = NOTIFICATION_ID; - if (message.isEmergencyAlertMessage() || CellBroadcastConfigService - .isOperatorDefinedEmergencyId(message.getMessageIdentifier())) { - // Emergency: open notification immediately - notification.fullScreenIntent = pi; - // use default notification lights (CellBroadcastAlertAudio plays sound/vibration) - notification.defaults = Notification.DEFAULT_LIGHTS; + PendingIntent pi = PendingIntent.getActivity(this, 0, createDisplayMessageIntent( + this, CellBroadcastListActivity.class, message, notificationId), 0); + + // use default sound/vibration/lights for non-emergency broadcasts + Notification.Builder builder = new Notification.Builder(this) + .setSmallIcon(R.drawable.stat_color_warning) + .setTicker(channelName) + .setWhen(System.currentTimeMillis()) + .setContentIntent(pi) + .setDefaults(Notification.DEFAULT_ALL); + + builder.setDefaults(Notification.DEFAULT_ALL); + + // increment unread alert count (decremented when user dismisses alert dialog) + int unreadCount = CellBroadcastReceiverApp.incrementUnreadAlertCount(); + if (unreadCount > 1) { + // use generic count of unread broadcasts if more than one unread + builder.setContentTitle(getString(R.string.notification_multiple_title)); + builder.setContentText(getString(R.string.notification_multiple, unreadCount)); } else { - // use default sound/vibration/lights for non-emergency broadcasts - notification.defaults = Notification.DEFAULT_ALL; + builder.setContentTitle(channelName).setContentText(messageBody); } Log.i(TAG, "addToNotificationBar notificationId: " + notificationId); @@ -208,15 +277,16 @@ public class CellBroadcastAlertService extends Service { NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(notificationId, notification); + notificationManager.notify(notificationId, builder.getNotification()); } - static Intent createDisplayMessageIntent(Context context, + static Intent createDisplayMessageIntent(Context context, Class intentClass, CellBroadcastMessage message, int notificationId) { // Trigger the list activity to fire up a dialog that shows the received messages - Intent intent = new Intent(context, CellBroadcastListActivity.class); + Intent intent = new Intent(context, intentClass); intent.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, message); intent.putExtra(SMS_CB_NOTIFICATION_ID_EXTRA, notificationId); + intent.putExtra(NEW_ALERT_EXTRA, true); // This line is needed to make this intent compare differently than the other intents // created here for other messages. Without this line, the PendingIntent always gets the diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertWakeLock.java b/src/com/android/cellbroadcastreceiver/CellBroadcastAlertWakeLock.java deleted file mode 100644 index 2d23031cf..000000000 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastAlertWakeLock.java +++ /dev/null @@ -1,52 +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.content.Context; -import android.os.PowerManager; - -/** - * Hold a wakelock while showing the alert dialog and playing sound. - */ -class CellBroadcastAlertWakeLock { - - private static PowerManager.WakeLock sCpuWakeLock; - - private CellBroadcastAlertWakeLock() {} - - static void acquireCpuWakeLock(Context context) { - if (sCpuWakeLock != null) { - return; - } - - PowerManager pm = - (PowerManager) context.getSystemService(Context.POWER_SERVICE); - - sCpuWakeLock = pm.newWakeLock( - PowerManager.PARTIAL_WAKE_LOCK | - PowerManager.ACQUIRE_CAUSES_WAKEUP | - PowerManager.ON_AFTER_RELEASE, "CellBroadcastAlertWakeLock"); - sCpuWakeLock.acquire(); - } - - static void releaseCpuLock() { - if (sCpuWakeLock != null) { - sCpuWakeLock.release(); - sCpuWakeLock = null; - } - } -} diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java b/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java index 112dfc7f9..53674ebe4 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastConfigService.java @@ -22,12 +22,11 @@ import android.content.SharedPreferences; import android.content.res.Resources; import android.os.SystemProperties; import android.preference.PreferenceManager; -import android.telephony.SmsCbConstants; import android.telephony.SmsManager; import android.text.TextUtils; import android.util.Log; -import com.android.internal.telephony.gsm.SmsCbHeader; +import com.android.internal.telephony.gsm.SmsCbConstants; import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; @@ -51,7 +50,7 @@ public class CellBroadcastConfigService extends IntentService { super(TAG); // use class name for worker thread name } - private void setChannelRange(SmsManager manager, String ranges, boolean enable) { + private static void setChannelRange(SmsManager manager, String ranges, boolean enable) { try { for (String channelRange : ranges.split(",")) { int dashIndex = channelRange.indexOf('-'); diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastListAdapter.java b/src/com/android/cellbroadcastreceiver/CellBroadcastCursorAdapter.java index d0b7964f1..9641c64fc 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastListAdapter.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastCursorAdapter.java @@ -26,11 +26,11 @@ import android.widget.CursorAdapter; /** * The back-end data adapter for {@link CellBroadcastListActivity}. */ -public class CellBroadcastListAdapter extends CursorAdapter { - private static final String TAG = "CellBroadcastListAdapter"; +public class CellBroadcastCursorAdapter extends CursorAdapter { - public CellBroadcastListAdapter(Context context, Cursor cursor) { - super(context, cursor, true); + public CellBroadcastCursorAdapter(Context context, Cursor cursor) { + // don't set FLAG_AUTO_REQUERY or FLAG_REGISTER_CONTENT_OBSERVER + super(context, cursor, 0); } /** @@ -41,6 +41,7 @@ public class CellBroadcastListAdapter extends CursorAdapter { * @param parent The parent to which the new view is attached to * @return the newly created view. */ + @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { CellBroadcastMessage message = CellBroadcastMessage.createFromCursor(cursor); @@ -59,6 +60,7 @@ public class CellBroadcastListAdapter extends CursorAdapter { * @param cursor The cursor from which to get the data. The cursor is already * moved to the correct position. */ + @Override public void bindView(View view, Context context, Cursor cursor) { CellBroadcastMessage message = CellBroadcastMessage.createFromCursor(cursor); CellBroadcastListItem listItem = (CellBroadcastListItem) view; diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastCursorLoader.java b/src/com/android/cellbroadcastreceiver/CellBroadcastCursorLoader.java new file mode 100644 index 000000000..2501336bc --- /dev/null +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastCursorLoader.java @@ -0,0 +1,181 @@ +/* + * 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 android.content.AsyncTaskLoader; +import android.content.CancellationSignal; +import android.content.Context; +import android.content.OperationCanceledException; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; + +/** + * Async task loader for Cell Broadcast database cursors. Based on + * {@link android.content.CursorLoader}, which is used to query content providers. + */ +class CellBroadcastCursorLoader extends AsyncTaskLoader<Cursor> { + + static final String TAG = "CellBroadcastCursorLoader"; + static final String TABLE_NAME = "broadcasts"; + + private SQLiteDatabase mDatabase; + + Cursor mCursor; + CancellationSignal mCancellationSignal; + + /** + * Creates an empty cell broadcast cursor loader. The query is hardcoded into the class. + */ + public CellBroadcastCursorLoader(Context context) { + super(context); + } + + /** + * Called on a worker thread to perform the actual load and to return the result of the load + * operation. + * + * @return The result of the load operation. + * @throws android.content.OperationCanceledException + * if the load is canceled during execution. + * @see #isLoadInBackgroundCanceled + * @see #cancelLoadInBackground + * @see #onCanceled + */ + @Override + public Cursor loadInBackground() { + synchronized (this) { + if (isLoadInBackgroundCanceled()) { + throw new OperationCanceledException(); + } + mCancellationSignal = new CancellationSignal(); + } + try { + if (mDatabase == null) { + if (DBG) Log.d(TAG, "loadInBackground: opening SQLite database"); + mDatabase = new CellBroadcastDatabase.DatabaseHelper(getContext()) + .getReadableDatabase(); + } + Cursor cursor = mDatabase.query(false, TABLE_NAME, + CellBroadcastDatabase.Columns.QUERY_COLUMNS, null, null, null, null, + CellBroadcastDatabase.Columns.DELIVERY_TIME + " DESC", null, + mCancellationSignal); + + if (cursor != null) { + // Ensure the cursor window is filled + int count = cursor.getCount(); + if (DBG) Log.d(TAG, "loadInBackground() cursor row count = " + count); + } + return cursor; + } finally { + synchronized (this) { + mCancellationSignal = null; + } + } + } + + @Override + public void cancelLoadInBackground() { + super.cancelLoadInBackground(); + + synchronized (this) { + if (mCancellationSignal != null) { + mCancellationSignal.cancel(); + } + } + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + if (DBG) Log.d(TAG, "isReset() is true, closing cursor and returning"); + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + return; + } + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + if (DBG) Log.d(TAG, "isStarted() is true, delivering result"); + super.deliverResult(cursor); + } else { + if (DBG) Log.d(TAG, "isStarted() is false, not delivering!!!"); + } + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + if (DBG) Log.d(TAG, "closing the old cursor..."); + oldCursor.close(); + } + } + + /** + * Starts an asynchronous query of the database. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + protected void onStartLoading() { + if (mCursor != null) { + Log.d(TAG, "onStartLoading() delivering existing cursor"); + deliverResult(mCursor); + } + if (takeContentChanged() || mCursor == null) { + Log.d(TAG, "onStartLoading() calling forceLoad()"); + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + protected void onStopLoading() { + // Attempt to cancel the current load task if possible. + cancelLoad(); + Log.d(TAG, "onStopLoading() called cancelLoad()"); + } + + @Override + public void onCanceled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + Log.d(TAG, "onCanceled() closed the cursor"); + } + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + Log.d(TAG, "onReset() closed the cursor"); + } + mCursor = null; + } +} diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastDatabase.java b/src/com/android/cellbroadcastreceiver/CellBroadcastDatabase.java index a27be4b76..0eb48c292 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastDatabase.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastDatabase.java @@ -16,21 +16,37 @@ package com.android.cellbroadcastreceiver; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbEtwsInfo; +import android.telephony.SmsCbMessage; +import android.util.Log; + +import com.android.internal.telephony.gsm.SmsCbConstants; public class CellBroadcastDatabase { - private static final String TAG = "CellBroadcastDatabase"; + // package local for efficient access from inner class + static final String TAG = "CellBroadcastDatabase"; private CellBroadcastDatabase() {} static final String DATABASE_NAME = "cell_broadcasts.db"; static final String TABLE_NAME = "broadcasts"; - static final int DATABASE_VERSION = 1; + /** Temporary table for upgrading the database version. */ + static final String TEMP_TABLE_NAME = "old_broadcasts"; + + /** + * Database version 1: initial version + * Database version 2-9: (reserved for OEM database customization) + * Database version 10: adds ETWS and CMAS columns and CDMA support + */ + static final int DATABASE_VERSION = 10; static final class Columns implements BaseColumns { @@ -49,16 +65,43 @@ public class CellBroadcastDatabase { public static final String SERIAL_NUMBER = "serial_number"; /** - * Message code. + * PLMN of broadcast sender. (SERIAL_NUMBER + PLMN + LAC + CID) uniquely identifies a + * broadcast for duplicate detection purposes. + * <P>Type: TEXT</P> + */ + public static final String PLMN = "plmn"; + + /** + * Location Area (GSM) or Service Area (UMTS) of broadcast sender. Unused for CDMA. + * Only included if Geographical Scope of message is not PLMN wide (01). + * <P>Type: INTEGER</P> + */ + public static final String LAC = "lac"; + + /** + * Cell ID of message sender (GSM/UMTS). Unused for CDMA. Only included when the + * Geographical Scope of message is cell wide (00 or 11). * <P>Type: INTEGER</P> */ - public static final String MESSAGE_CODE = "message_code"; + public static final String CID = "cid"; /** - * Message identifier. + * Message code (OBSOLETE: merged into SERIAL_NUMBER). * <P>Type: INTEGER</P> */ - public static final String MESSAGE_IDENTIFIER = "message_id"; + public static final String V1_MESSAGE_CODE = "message_code"; + + /** + * Message identifier (OBSOLETE: renamed to SERVICE_CATEGORY). + * <P>Type: INTEGER</P> + */ + public static final String V1_MESSAGE_IDENTIFIER = "message_id"; + + /** + * Service category (GSM/UMTS message identifier, CDMA service category). + * <P>Type: INTEGER</P> + */ + public static final String SERVICE_CATEGORY = "service_category"; /** * Message language code. @@ -85,18 +128,83 @@ public class CellBroadcastDatabase { public static final String MESSAGE_READ = "read"; /** + * Message format (3GPP or 3GPP2). + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_FORMAT = "format"; + + /** + * Message priority (including emergency). + * <P>Type: INTEGER</P> + */ + public static final String MESSAGE_PRIORITY = "priority"; + + /** + * ETWS warning type (ETWS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String ETWS_WARNING_TYPE = "etws_warning_type"; + + /** + * CMAS message class (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_MESSAGE_CLASS = "cmas_message_class"; + + /** + * CMAS category (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_CATEGORY = "cmas_category"; + + /** + * CMAS response type (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_RESPONSE_TYPE = "cmas_response_type"; + + /** + * CMAS severity (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_SEVERITY = "cmas_severity"; + + /** + * CMAS urgency (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_URGENCY = "cmas_urgency"; + + /** + * CMAS certainty (CMAS alerts only). + * <P>Type: INTEGER</P> + */ + public static final String CMAS_CERTAINTY = "cmas_certainty"; + + /** * Query for list view adapter. */ static final String[] QUERY_COLUMNS = { _ID, GEOGRAPHICAL_SCOPE, + PLMN, + LAC, + CID, SERIAL_NUMBER, - MESSAGE_CODE, - MESSAGE_IDENTIFIER, + SERVICE_CATEGORY, LANGUAGE_CODE, MESSAGE_BODY, DELIVERY_TIME, MESSAGE_READ, + MESSAGE_FORMAT, + MESSAGE_PRIORITY, + ETWS_WARNING_TYPE, + CMAS_MESSAGE_CLASS, + CMAS_CATEGORY, + CMAS_RESPONSE_TYPE, + CMAS_SEVERITY, + CMAS_URGENCY, + CMAS_CERTAINTY }; } @@ -104,13 +212,24 @@ public class CellBroadcastDatabase { static final int COLUMN_ID = 0; static final int COLUMN_GEOGRAPHICAL_SCOPE = 1; - static final int COLUMN_SERIAL_NUMBER = 2; - static final int COLUMN_MESSAGE_CODE = 3; - static final int COLUMN_MESSAGE_IDENTIFIER = 4; - static final int COLUMN_LANGUAGE_CODE = 5; - static final int COLUMN_MESSAGE_BODY = 6; - static final int COLUMN_DELIVERY_TIME = 7; - static final int COLUMN_MESSAGE_READ = 8; + static final int COLUMN_PLMN = 2; + static final int COLUMN_LAC = 3; + static final int COLUMN_CID = 4; + static final int COLUMN_SERIAL_NUMBER = 5; + static final int COLUMN_SERVICE_CATEGORY = 6; // was COLUMN_MESSAGE_IDENTIFIER + static final int COLUMN_LANGUAGE_CODE = 7; + static final int COLUMN_MESSAGE_BODY = 8; + static final int COLUMN_DELIVERY_TIME = 9; + static final int COLUMN_MESSAGE_READ = 10; + static final int COLUMN_MESSAGE_FORMAT = 11; + static final int COLUMN_MESSAGE_PRIORITY = 12; + static final int COLUMN_ETWS_WARNING_TYPE = 13; + static final int COLUMN_CMAS_MESSAGE_CLASS = 14; + static final int COLUMN_CMAS_CATEGORY = 15; + static final int COLUMN_CMAS_RESPONSE_TYPE = 16; + static final int COLUMN_CMAS_SEVERITY = 17; + static final int COLUMN_CMAS_URGENCY = 18; + static final int COLUMN_CMAS_CERTAINTY = 19; static class DatabaseHelper extends SQLiteOpenHelper { @@ -123,28 +242,242 @@ public class CellBroadcastDatabase { db.execSQL("CREATE TABLE " + TABLE_NAME + " (" + Columns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + Columns.GEOGRAPHICAL_SCOPE + " INTEGER," + + Columns.PLMN + " TEXT," + + Columns.LAC + " INTEGER," + + Columns.CID + " INTEGER," + Columns.SERIAL_NUMBER + " INTEGER," - + Columns.MESSAGE_CODE + " INTEGER," - + Columns.MESSAGE_IDENTIFIER + " INTEGER," + + Columns.SERVICE_CATEGORY + " INTEGER," + Columns.LANGUAGE_CODE + " TEXT," + Columns.MESSAGE_BODY + " TEXT," + Columns.DELIVERY_TIME + " INTEGER," - + Columns.MESSAGE_READ + " INTEGER);"); + + Columns.MESSAGE_READ + " INTEGER," + + Columns.MESSAGE_FORMAT + " INTEGER," + + Columns.MESSAGE_PRIORITY + " INTEGER," + + Columns.ETWS_WARNING_TYPE + " INTEGER," + + Columns.CMAS_MESSAGE_CLASS + " INTEGER," + + Columns.CMAS_CATEGORY + " INTEGER," + + Columns.CMAS_RESPONSE_TYPE + " INTEGER," + + Columns.CMAS_SEVERITY + " INTEGER," + + Columns.CMAS_URGENCY + " INTEGER," + + Columns.CMAS_CERTAINTY + " INTEGER);"); } + /** Columns to copy on database upgrade. */ + private static final String[] COLUMNS_V1 = { + Columns.GEOGRAPHICAL_SCOPE, + Columns.SERIAL_NUMBER, + Columns.V1_MESSAGE_CODE, + Columns.V1_MESSAGE_IDENTIFIER, + Columns.LANGUAGE_CODE, + Columns.MESSAGE_BODY, + Columns.DELIVERY_TIME, + Columns.MESSAGE_READ, + }; + + private static final int COLUMN_V1_GEOGRAPHICAL_SCOPE = 0; + private static final int COLUMN_V1_SERIAL_NUMBER = 1; + private static final int COLUMN_V1_MESSAGE_CODE = 2; + private static final int COLUMN_V1_MESSAGE_IDENTIFIER = 3; + private static final int COLUMN_V1_LANGUAGE_CODE = 4; + private static final int COLUMN_V1_MESSAGE_BODY = 5; + private static final int COLUMN_V1_DELIVERY_TIME = 6; + private static final int COLUMN_V1_MESSAGE_READ = 7; + @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // ignored for now + if (oldVersion == newVersion) { + return; + } + Log.i(TAG, "Upgrading DB from version " + oldVersion + " to " + newVersion); + + if (oldVersion == 1) { + db.beginTransaction(); + try { + // Step 1: rename original table + db.execSQL("DROP TABLE IF EXISTS " + TEMP_TABLE_NAME); + db.execSQL("ALTER TABLE " + TABLE_NAME + " RENAME TO " + TEMP_TABLE_NAME); + + // Step 2: create new table and indices + onCreate(db); + + // Step 3: copy each message into the new table + Cursor cursor = db.query(TEMP_TABLE_NAME, COLUMNS_V1, null, null, null, null, + null); + try { + while (cursor.moveToNext()) { + upgradeMessageV1ToV2(db, cursor); + } + } finally { + cursor.close(); + } + + // Step 4: drop the original table and commit transaction + db.execSQL("DROP TABLE " + TEMP_TABLE_NAME); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + oldVersion = 2; + } } - } - /** - * Returns a Cursor for the list view adapter, in reverse chronological order. - * @param db an open readable database - * @return the cursor for the list view adapter - */ - static Cursor getCursor(SQLiteDatabase db) { - return db.query(false, TABLE_NAME, Columns.QUERY_COLUMNS, - null, null, null, null, Columns.DELIVERY_TIME + " DESC", null); + /** + * Upgrades a single broadcast message from version 1 to version 2. + */ + private static void upgradeMessageV1ToV2(SQLiteDatabase db, Cursor cursor) { + int geographicalScope = cursor.getInt(COLUMN_V1_GEOGRAPHICAL_SCOPE); + int updateNumber = cursor.getInt(COLUMN_V1_SERIAL_NUMBER); + int messageCode = cursor.getInt(COLUMN_V1_MESSAGE_CODE); + int messageId = cursor.getInt(COLUMN_V1_MESSAGE_IDENTIFIER); + String languageCode = cursor.getString(COLUMN_V1_LANGUAGE_CODE); + String messageBody = cursor.getString(COLUMN_V1_MESSAGE_BODY); + long deliveryTime = cursor.getLong(COLUMN_V1_DELIVERY_TIME); + boolean isRead = (cursor.getInt(COLUMN_V1_MESSAGE_READ) != 0); + + int serialNumber = ((geographicalScope & 0x03) << 14) + | ((messageCode & 0x3ff) << 4) | (updateNumber & 0x0f); + + ContentValues cv = new ContentValues(16); + cv.put(Columns.GEOGRAPHICAL_SCOPE, geographicalScope); + cv.put(Columns.SERIAL_NUMBER, serialNumber); + cv.put(Columns.SERVICE_CATEGORY, messageId); + cv.put(Columns.LANGUAGE_CODE, languageCode); + cv.put(Columns.MESSAGE_BODY, messageBody); + cv.put(Columns.DELIVERY_TIME, deliveryTime); + cv.put(Columns.MESSAGE_READ, isRead); + cv.put(Columns.MESSAGE_FORMAT, SmsCbMessage.MESSAGE_FORMAT_3GPP); + + int etwsWarningType = SmsCbEtwsInfo.ETWS_WARNING_TYPE_UNKNOWN; + int cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; + int cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + int cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + int cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + switch (messageId) { + case SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING: + etwsWarningType = SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE; + break; + + case SmsCbConstants.MESSAGE_ID_ETWS_TSUNAMI_WARNING: + etwsWarningType = SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI; + break; + + case SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_AND_TSUNAMI_WARNING: + etwsWarningType = SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI; + break; + + case SmsCbConstants.MESSAGE_ID_ETWS_TEST_MESSAGE: + etwsWarningType = SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE; + break; + + case SmsCbConstants.MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE: + etwsWarningType = SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_EXTREME; + cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE; + cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_EXTREME; + cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE; + cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_EXTREME; + cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_EXPECTED; + cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_EXTREME; + cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_EXPECTED; + cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_SEVERE; + cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE; + cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_SEVERE; + cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE; + cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_SEVERE; + cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_EXPECTED; + cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + cmasSeverity = SmsCbCmasInfo.CMAS_SEVERITY_SEVERE; + cmasUrgency = SmsCbCmasInfo.CMAS_URGENCY_EXPECTED; + cmasCertainty = SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE; + break; + + case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE: + cmasMessageClass = SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE; + break; + } + + if (etwsWarningType != SmsCbEtwsInfo.ETWS_WARNING_TYPE_UNKNOWN + || cmasMessageClass != SmsCbCmasInfo.CMAS_CLASS_UNKNOWN) { + cv.put(Columns.MESSAGE_PRIORITY, SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY); + } else { + cv.put(Columns.MESSAGE_PRIORITY, SmsCbMessage.MESSAGE_PRIORITY_NORMAL); + } + + if (etwsWarningType != SmsCbEtwsInfo.ETWS_WARNING_TYPE_UNKNOWN) { + cv.put(Columns.ETWS_WARNING_TYPE, etwsWarningType); + } + + if (cmasMessageClass != SmsCbCmasInfo.CMAS_CLASS_UNKNOWN) { + cv.put(Columns.CMAS_MESSAGE_CLASS, cmasMessageClass); + } + + if (cmasSeverity != SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN) { + cv.put(Columns.CMAS_SEVERITY, cmasSeverity); + } + + if (cmasUrgency != SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN) { + cv.put(Columns.CMAS_URGENCY, cmasUrgency); + } + + if (cmasCertainty != SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN) { + cv.put(Columns.CMAS_CERTAINTY, cmasCertainty); + } + + db.insert(TABLE_NAME, null, cv); + } } }
\ No newline at end of file diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastDatabaseService.java b/src/com/android/cellbroadcastreceiver/CellBroadcastDatabaseService.java index d27e21e67..2e1d5b521 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastDatabaseService.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastDatabaseService.java @@ -20,8 +20,11 @@ import android.app.IntentService; import android.content.ContentValues; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.util.Log; +import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; + /** * Service to update the SQLite database to add a new broadcast message, * or to delete one or all previously received broadcasts. @@ -49,46 +52,53 @@ public class CellBroadcastDatabaseService extends IntentService { public static final String DATABASE_DELIVERY_TIME_EXTRA = "com.android.cellbroadcastreceiver.DATABASE_DELIVERY_TIME"; + /** Identifier for getExtra() to tell it to decrement the unread alert count. */ + public static final String DECREMENT_UNREAD_ALERT_COUNT = + "com.android.cellbroadcastreceiver.DECREMENT_UNREAD_ALERT_COUNT"; + private SQLiteDatabase mBroadcastDb; /** Callback for the active list activity when the contents change. */ - private static CellBroadcastListActivity sActiveListActivity; + private static CellBroadcastListActivity.CursorLoaderListFragment sActiveListFragment; public CellBroadcastDatabaseService() { super(TAG); // use class name for worker thread name } @Override - public void onCreate() { - super.onCreate(); - - if (mBroadcastDb == null) { - CellBroadcastDatabase.DatabaseHelper helper = - new CellBroadcastDatabase.DatabaseHelper(this); - mBroadcastDb = helper.getWritableDatabase(); - } - } - - @Override public void onDestroy() { super.onDestroy(); if (mBroadcastDb != null) { + if (DBG) Log.d(TAG, "onDestroy: closing mBroadcastDb..."); mBroadcastDb.close(); mBroadcastDb = null; } } - static void setActiveListActivity(CellBroadcastListActivity activity) { - sActiveListActivity = activity; + static void setActiveListFragment(CellBroadcastListActivity.CursorLoaderListFragment fragment) { + sActiveListFragment = fragment; } @Override public void onHandleIntent(Intent intent) { - // TODO: security check to detect malicious broadcast injections + // open the database on the worker thread + if (mBroadcastDb == null) { + if (DBG) Log.d(TAG, "onHandleIntent: opening mBroadcastDb..."); + CellBroadcastDatabase.DatabaseHelper helper = + new CellBroadcastDatabase.DatabaseHelper(this); + try { + mBroadcastDb = helper.getWritableDatabase(); + } catch (SQLiteException e) { + Log.e(TAG, "SQLite exception trying to open broadcasts db", e); + return; + } + } + String action = intent.getAction(); boolean notifyActiveListActivity = false; if (ACTION_INSERT_NEW_BROADCAST.equals(action)) { + if (DBG) Log.d(TAG, "onHandleIntent: ACTION_INSERT_NEW_BROADCAST"); CellBroadcastMessage cbm = intent.getParcelableExtra( CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA); if (cbm == null) { @@ -96,6 +106,8 @@ public class CellBroadcastDatabaseService extends IntentService { return; } + if (DBG) Log.d(TAG, "new broadcast deliveryTime = " + cbm.getDeliveryTime()); + ContentValues cv = cbm.getContentValues(); long rowId = mBroadcastDb.insert(CellBroadcastDatabase.TABLE_NAME, null, cv); if (rowId == -1) { @@ -104,6 +116,7 @@ public class CellBroadcastDatabaseService extends IntentService { notifyActiveListActivity = true; } } else if (ACTION_DELETE_BROADCAST.equals(action)) { + if (DBG) Log.d(TAG, "onHandleIntent: ACTION_DELETE_BROADCAST"); long rowId = intent.getLongExtra(DATABASE_ROW_ID_EXTRA, -1); if (rowId == -1) { Log.e(TAG, "ACTION_DELETE_BROADCAST missing row ID to delete"); @@ -115,11 +128,17 @@ public class CellBroadcastDatabaseService extends IntentService { new String[]{Long.toString(rowId)}); if (rowCount != 0) { notifyActiveListActivity = true; + if (intent.getBooleanExtra(DECREMENT_UNREAD_ALERT_COUNT, false)) { + CellBroadcastReceiverApp.decrementUnreadAlertCount(); + } } } else if (ACTION_DELETE_ALL_BROADCASTS.equals(action)) { + if (DBG) Log.d(TAG, "onHandleIntent: ACTION_DELETE_ALL_BROADCASTS"); mBroadcastDb.delete(CellBroadcastDatabase.TABLE_NAME, null, null); + CellBroadcastReceiverApp.resetUnreadAlertCount(); notifyActiveListActivity = true; } else if (ACTION_MARK_BROADCAST_READ.equals(action)) { + if (DBG) Log.d(TAG, "onHandleIntent: ACTION_MARK_BROADCAST_READ"); long rowId = intent.getLongExtra(DATABASE_ROW_ID_EXTRA, -1); long deliveryTime = intent.getLongExtra(DATABASE_DELIVERY_TIME_EXTRA, -1); if (rowId == -1 && deliveryTime == -1) { @@ -128,24 +147,33 @@ public class CellBroadcastDatabaseService extends IntentService { } ContentValues cv = new ContentValues(1); cv.put(CellBroadcastDatabase.Columns.MESSAGE_READ, 1); - int rowCount; + + // For new alerts, select by delivery time because the row ID is not known. + String whereClause; + String[] whereArgs; if (rowId != -1) { - rowCount = mBroadcastDb.update(CellBroadcastDatabase.TABLE_NAME, cv, - CellBroadcastDatabase.Columns._ID + "=?", - new String[]{Long.toString(rowId)}); + whereClause = CellBroadcastDatabase.Columns._ID + "=?"; + whereArgs = new String[]{Long.toString(rowId)}; } else { - rowCount = mBroadcastDb.update(CellBroadcastDatabase.TABLE_NAME, cv, - CellBroadcastDatabase.Columns.DELIVERY_TIME + "=?", - new String[]{Long.toString(deliveryTime)}); + whereClause = CellBroadcastDatabase.Columns.DELIVERY_TIME + "=?"; + whereArgs = new String[]{Long.toString(deliveryTime)}; } + + int rowCount = mBroadcastDb.update(CellBroadcastDatabase.TABLE_NAME, cv, + whereClause, whereArgs); + if (rowCount != 0) { notifyActiveListActivity = true; + } else { + Log.e(TAG, "MARK_BROADCAST_READ failed, rowId: " + rowId + " deliveryTime: " + + deliveryTime); } } else { Log.e(TAG, "ignoring unexpected Intent with action " + action); } - if (notifyActiveListActivity && sActiveListActivity != null) { - sActiveListActivity.databaseContentChanged(); + if (notifyActiveListActivity && sActiveListFragment != null) { + if (DBG) Log.d(TAG, "notifying databaseContentChanged() for " + action); + sActiveListFragment.databaseContentChanged(); } } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java b/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java index eafeec298..e32b491aa 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastListActivity.java @@ -16,247 +16,301 @@ package com.android.cellbroadcastreceiver; +import android.app.Activity; import android.app.AlertDialog; -import android.app.ListActivity; +import android.app.FragmentManager; +import android.app.ListFragment; +import android.app.LoaderManager; import android.app.NotificationManager; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; +import android.content.Loader; import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; +import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnCreateContextMenuListener; +import android.view.ViewGroup; +import android.widget.CursorAdapter; import android.widget.ListView; import android.widget.TextView; /** - * This activity provides a list view of received cell broadcasts. + * This activity provides a list view of received cell broadcasts. Most of the work is handled + * in the inner CursorLoaderListFragment class. */ -public class CellBroadcastListActivity extends ListActivity { - private static final String TAG = "CellBroadcastListActivity"; +public class CellBroadcastListActivity extends Activity { + // package local for efficient access from inner class + static final String TAG = "CellBroadcastListActivity"; - // IDs of the main menu items. - public static final int MENU_DELETE_ALL = 3; - public static final int MENU_PREFERENCES = 4; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - // IDs of the context menu items for the list of broadcasts. - public static final int MENU_DELETE = 0; - public static final int MENU_VIEW = 1; + // Dismiss the notification that brought us here (if any). + ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)) + .cancel(CellBroadcastAlertService.NOTIFICATION_ID); - private CellBroadcastListAdapter mListAdapter; + FragmentManager fm = getFragmentManager(); - private SQLiteDatabase mBroadcastDb; + // Create the list fragment and add it as our sole content. + if (fm.findFragmentById(android.R.id.content) == null) { + CursorLoaderListFragment listFragment = new CursorLoaderListFragment(); + fm.beginTransaction().add(android.R.id.content, listFragment).commit(); + } + } - private Cursor mAdapterCursor; + /** + * List fragment queries SQLite database on worker thread. + */ + public static class CursorLoaderListFragment extends ListFragment + implements LoaderManager.LoaderCallbacks<Cursor> { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.cell_broadcast_list_screen); + // IDs of the main menu items. + private static final int MENU_DELETE_ALL = 3; + private static final int MENU_PREFERENCES = 4; + + // IDs of the context menu items (package local, accessed from inner DeleteThreadListener). + static final int MENU_DELETE = 0; + static final int MENU_VIEW = 1; + + // This is the Adapter being used to display the list's data. + CursorAdapter mAdapter; - ListView listView = getListView(); - listView.setOnCreateContextMenuListener(mOnCreateContextMenuListener); + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - if (mBroadcastDb == null) { - CellBroadcastDatabase.DatabaseHelper helper = - new CellBroadcastDatabase.DatabaseHelper(this); - mBroadcastDb = helper.getReadableDatabase(); + // We have a menu item to show in action bar. + setHasOptionsMenu(true); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); } - if (mAdapterCursor == null) { - mAdapterCursor = CellBroadcastDatabase.getCursor(mBroadcastDb); + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.cell_broadcast_list_screen, container, false); } - mListAdapter = new CellBroadcastListAdapter(this, mAdapterCursor); - setListAdapter(mListAdapter); + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); - CellBroadcastDatabaseService.setActiveListActivity(this); + // Tell database service to notify us when a new broadcast arrives. + CellBroadcastDatabaseService.setActiveListFragment(this); - parseIntent(getIntent()); - } + // Set context menu for long-press. + ListView listView = getListView(); + listView.setOnCreateContextMenuListener(mOnCreateContextMenuListener); - @Override - protected void onDestroy() { - if (mAdapterCursor != null) { - mAdapterCursor.close(); - mAdapterCursor = null; + // Create a cursor adapter to display the loaded data. + mAdapter = new CellBroadcastCursorAdapter(getActivity(), null); + setListAdapter(mAdapter); } - if (mBroadcastDb != null) { - mBroadcastDb.close(); - mBroadcastDb = null; + + @Override + public void onDestroy() { + super.onDestroy(); + CellBroadcastDatabaseService.setActiveListFragment(null); } - CellBroadcastDatabaseService.setActiveListActivity(null); - super.onDestroy(); - } - /** Callback from CellBroadcastDatabaseService after content changes. */ - void databaseContentChanged() { - runOnUiThread(new Runnable() { - public void run() { - mAdapterCursor.requery(); - } - }); - } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon( + android.R.drawable.ic_menu_delete); + menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon( + android.R.drawable.ic_menu_preferences); + } - @Override - protected void onNewIntent(Intent intent) { - // TODO: how do multiple messages stack together? - // removeDialog(DIALOG_SHOW_MESSAGE); - parseIntent(intent); - } + @Override + public void onPrepareOptionsMenu(Menu menu) { + menu.findItem(MENU_DELETE_ALL).setVisible(!mAdapter.isEmpty()); + } - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - menu.clear(); + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + CellBroadcastListItem cbli = (CellBroadcastListItem) v; + showDialogAndMarkRead(cbli.getMessage()); + } - if (mListAdapter.getCount() > 0) { - menu.add(0, MENU_DELETE_ALL, 0, R.string.menu_delete_all).setIcon( - android.R.drawable.ic_menu_delete); + @Override + public Loader<Cursor> onCreateLoader(int id, Bundle args) { + return new CellBroadcastCursorLoader(getActivity()); } - menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon( - android.R.drawable.ic_menu_preferences); + @Override + public void onLoadFinished(Loader<Cursor> loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + mAdapter.swapCursor(data); + } - return true; - } + @Override + public void onLoaderReset(Loader<Cursor> loader) { + // This is called when the last Cursor provided to onLoadFinished() + // above is about to be closed. We need to make sure we are no + // longer using it. + mAdapter.swapCursor(null); + } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch(item.getItemId()) { - case MENU_DELETE_ALL: - confirmDeleteThread(-1); - break; - - case MENU_PREFERENCES: - Intent intent = new Intent(this, CellBroadcastSettings.class); - startActivityIfNeeded(intent, -1); - break; - - default: - return true; + /** + * Callback from CellBroadcastDatabaseService after content changes. + */ + void databaseContentChanged() { + Loader<Cursor> loader = getLoaderManager().getLoader(0); + if (loader != null) { + loader.onContentChanged(); + } else { + Log.w(TAG, "databaseContentChanged() called, but loader is null"); + } } - return false; - } - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - Cursor cursor = mListAdapter.getCursor(); - if (cursor != null && cursor.getPosition() >= 0) { - showDialogAndMarkRead(cursor); + private void showDialogAndMarkRead(CellBroadcastMessage cbm) { + // show emergency alerts with the warning icon, but don't play alert tone + Intent i = new Intent(getActivity(), CellBroadcastAlertDialog.class); + i.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, cbm); + startActivity(i); } - } - private final OnCreateContextMenuListener mOnCreateContextMenuListener = - new OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - menu.add(0, MENU_VIEW, 0, R.string.menu_view); - menu.add(0, MENU_DELETE, 0, R.string.menu_delete); + private final OnCreateContextMenuListener mOnCreateContextMenuListener = + new OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + menu.setHeaderTitle(R.string.message_options); + menu.add(0, MENU_VIEW, 0, R.string.menu_view); + menu.add(0, MENU_DELETE, 0, R.string.menu_delete); + } + }; + + @Override + public boolean onContextItemSelected(MenuItem item) { + Cursor cursor = mAdapter.getCursor(); + if (cursor != null && cursor.getPosition() >= 0) { + switch (item.getItemId()) { + case MENU_DELETE: + // We need to decrement the unread alert count if deleting unread alert + boolean isUnread = + (cursor.getInt(CellBroadcastDatabase.COLUMN_MESSAGE_READ) == 0); + confirmDeleteThread(cursor.getLong(CellBroadcastDatabase.COLUMN_ID), + isUnread); + break; + + case MENU_VIEW: + showDialogAndMarkRead(CellBroadcastMessage.createFromCursor(cursor)); + break; + + default: + break; } - }; + } + return super.onContextItemSelected(item); + } - @Override - public boolean onContextItemSelected(MenuItem item) { - Cursor cursor = mListAdapter.getCursor(); - if (cursor != null && cursor.getPosition() >= 0) { - switch (item.getItemId()) { - case MENU_DELETE: - confirmDeleteThread(cursor.getLong(CellBroadcastDatabase.COLUMN_ID)); + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case MENU_DELETE_ALL: + confirmDeleteThread(-1, false); break; - case MENU_VIEW: - showDialogAndMarkRead(cursor); + case MENU_PREFERENCES: + Intent intent = new Intent(getActivity(), CellBroadcastSettings.class); + startActivity(intent); break; default: - break; + return true; } + return false; } - return super.onContextItemSelected(item); - } - private void showDialogAndMarkRead(Cursor cursor) { - CellBroadcastMessage cbm = CellBroadcastMessage.createFromCursor(cursor); - boolean isAlertMessage = cbm.isPublicAlertMessage() || CellBroadcastConfigService - .isOperatorDefinedEmergencyId(cbm.getMessageIdentifier()); - // show emergency alerts with the warning icon, but don't play alert tone - CellBroadcastAlertDialog dialog = new CellBroadcastAlertDialog(this, - cbm.getDialogTitleResource(), cbm.getMessageBody(), - isAlertMessage, cbm.getDeliveryTime()); - dialog.show(); - } - - /** - * Start the process of putting up a dialog to confirm deleting a broadcast. - * @param rowId the row ID of the broadcast to delete, or -1 to delete all broadcasts - */ - public void confirmDeleteThread(long rowId) { - DeleteThreadListener listener = new DeleteThreadListener(rowId); - confirmDeleteThreadDialog(listener, (rowId == -1), this); - } + /** + * Start the process of putting up a dialog to confirm deleting a broadcast. + * @param rowId the row ID of the broadcast to delete, or -1 to delete all broadcasts + * @param unread true if the alert was not already marked as read + */ + public void confirmDeleteThread(long rowId, boolean unread) { + DeleteThreadListener listener = new DeleteThreadListener(rowId, unread); + confirmDeleteThreadDialog(listener, (rowId == -1), getActivity()); + } - /** - * Build and show the proper delete broadcast dialog. The UI is slightly different - * depending on whether there are locked messages in the thread(s) and whether we're - * deleting a single broadcast or all broadcasts. - * @param listener gets called when the delete button is pressed - * @param deleteAll whether to show a single thread or all threads UI - * @param context used to load the various UI elements - */ - public static void confirmDeleteThreadDialog(DeleteThreadListener listener, - boolean deleteAll, Context context) { - View contents = View.inflate(context, R.layout.delete_broadcast_dialog_view, null); - TextView msg = (TextView)contents.findViewById(R.id.message); - msg.setText(deleteAll - ? R.string.confirm_delete_all_broadcasts - : R.string.confirm_delete_broadcast); - - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.confirm_dialog_title) - .setIcon(android.R.drawable.ic_dialog_alert) - .setCancelable(true) - .setPositiveButton(R.string.button_delete, listener) - .setNegativeButton(R.string.button_cancel, null) - .setView(contents) - .show(); - } + /** + * Build and show the proper delete broadcast dialog. The UI is slightly different + * depending on whether there are locked messages in the thread(s) and whether we're + * deleting a single broadcast or all broadcasts. + * @param listener gets called when the delete button is pressed + * @param deleteAll whether to show a single thread or all threads UI + * @param context used to load the various UI elements + */ + public static void confirmDeleteThreadDialog(DeleteThreadListener listener, + boolean deleteAll, Context context) { + View contents = View.inflate(context, R.layout.delete_broadcast_dialog_view, null); + TextView msg = (TextView)contents.findViewById(R.id.message); + msg.setText(deleteAll + ? R.string.confirm_delete_all_broadcasts + : R.string.confirm_delete_broadcast); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.confirm_dialog_title) + .setIcon(android.R.drawable.ic_dialog_alert) + .setCancelable(true) + .setPositiveButton(R.string.button_delete, listener) + .setNegativeButton(R.string.button_cancel, null) + .setView(contents) + .show(); + } - public class DeleteThreadListener implements OnClickListener { - private final long mRowId; + public class DeleteThreadListener implements OnClickListener { + private final long mRowId; + private final boolean mIsUnread; - public DeleteThreadListener(long rowId) { - mRowId = rowId; - } + public DeleteThreadListener(long rowId, boolean unread) { + mRowId = rowId; + mIsUnread = unread; + } - public void onClick(DialogInterface dialog, int whichButton) { - if (mRowId != -1) { + @Override + public void onClick(DialogInterface dialog, int whichButton) { // delete from database on a separate service thread - Intent dbWriteIntent = new Intent(CellBroadcastListActivity.this, + Intent dbWriteIntent = new Intent(getActivity(), CellBroadcastDatabaseService.class); - dbWriteIntent.setAction(CellBroadcastDatabaseService.ACTION_DELETE_BROADCAST); - dbWriteIntent.putExtra(CellBroadcastDatabaseService.DATABASE_ROW_ID_EXTRA, mRowId); - startService(dbWriteIntent); - } else { - // delete from database on a separate service thread - Intent dbWriteIntent = new Intent(CellBroadcastListActivity.this, - CellBroadcastDatabaseService.class); - dbWriteIntent.setAction(CellBroadcastDatabaseService.ACTION_DELETE_ALL_BROADCASTS); - startService(dbWriteIntent); + if (mRowId != -1) { + dbWriteIntent.setAction(CellBroadcastDatabaseService.ACTION_DELETE_BROADCAST); + dbWriteIntent.putExtra(CellBroadcastDatabaseService.DATABASE_ROW_ID_EXTRA, + mRowId); + if (mIsUnread) { + // decrement unread alert count after delete + dbWriteIntent.putExtra( + CellBroadcastDatabaseService.DECREMENT_UNREAD_ALERT_COUNT, true); + } + } else { + dbWriteIntent.setAction( + CellBroadcastDatabaseService.ACTION_DELETE_ALL_BROADCASTS); + } + getActivity().startService(dbWriteIntent); + dialog.dismiss(); } - dialog.dismiss(); } } - private void parseIntent(Intent intent) { + @Override + protected void onNewIntent(Intent intent) { if (intent == null) { return; } + Bundle extras = intent.getExtras(); if (extras == null) { return; @@ -270,12 +324,9 @@ public class CellBroadcastListActivity extends ListActivity { (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(notificationId); - boolean isEmergencyAlert = cbm.isPublicAlertMessage() || CellBroadcastConfigService - .isOperatorDefinedEmergencyId(cbm.getMessageIdentifier()); - - CellBroadcastAlertDialog dialog = new CellBroadcastAlertDialog(this, - cbm.getDialogTitleResource(), cbm.getMessageBody(), - isEmergencyAlert, cbm.getDeliveryTime()); - dialog.show(); + // launch the dialog activity to show the alert + Intent i = new Intent(this, CellBroadcastAlertDialog.class); + i.putExtra(CellBroadcastMessage.SMS_CB_MESSAGE_EXTRA, cbm); + startActivity(i); } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastListItem.java b/src/com/android/cellbroadcastreceiver/CellBroadcastListItem.java index 6f6423609..52aff0217 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastListItem.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastListItem.java @@ -17,24 +17,18 @@ package com.android.cellbroadcastreceiver; import android.content.Context; -import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.text.Spannable; import android.text.SpannableStringBuilder; -import android.text.style.StyleSpan; import android.util.AttributeSet; import android.view.accessibility.AccessibilityEvent; import android.widget.RelativeLayout; import android.widget.TextView; -import java.util.List; - /** - * This class manages the view for given conversation. + * This class manages the list item view for a single alert. */ public class CellBroadcastListItem extends RelativeLayout { - private static final String TAG = "CellBroadcastListItem"; - private static final boolean DEBUG = false; private CellBroadcastMessage mCbMessage; @@ -42,12 +36,6 @@ public class CellBroadcastListItem extends RelativeLayout { private TextView mMessageView; private TextView mDateView; - private static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD); - - public CellBroadcastListItem(Context context) { - super(context); - } - public CellBroadcastListItem(Context context, AttributeSet attrs) { super(context, attrs); } @@ -90,7 +78,7 @@ public class CellBroadcastListItem extends RelativeLayout { // Unread messages are shown in bold if (!message.isRead()) { - buf.setSpan(STYLE_BOLD, 0, buf.length(), + buf.setSpan(CellBroadcastMessage.STYLE_BOLD, 0, buf.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); } return buf; diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastMessage.java b/src/com/android/cellbroadcastreceiver/CellBroadcastMessage.java index e38aa022a..f9ff4ec54 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastMessage.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastMessage.java @@ -19,13 +19,17 @@ package com.android.cellbroadcastreceiver; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.graphics.Typeface; import android.os.Parcel; import android.os.Parcelable; -import android.telephony.SmsCbConstants; +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbEtwsInfo; +import android.telephony.SmsCbLocation; import android.telephony.SmsCbMessage; +import android.text.Spannable; +import android.text.SpannableStringBuilder; import android.text.format.DateUtils; - -import com.android.internal.telephony.gsm.SmsCbHeader; +import android.text.style.StyleSpan; /** * Application wrapper for {@link SmsCbMessage}. This is Parcelable so that @@ -40,51 +44,40 @@ public class CellBroadcastMessage implements Parcelable { public static final String SMS_CB_MESSAGE_EXTRA = "com.android.cellbroadcastreceiver.SMS_CB_MESSAGE"; - private final int mGeographicalScope; - private final int mSerialNumber; - private final int mMessageCode; - private final int mMessageIdentifier; - private final String mLanguageCode; - private final String mMessageBody; + /** Bold style for formatting CMAS headers and unread message items. */ + static final StyleSpan STYLE_BOLD = new StyleSpan(Typeface.BOLD); + + /** SmsCbMessage. */ + private final SmsCbMessage mSmsCbMessage; + private final long mDeliveryTime; private boolean mIsRead; - public CellBroadcastMessage(SmsCbMessage message) { - mGeographicalScope = message.getGeographicalScope(); - mSerialNumber = message.getUpdateNumber(); - mMessageCode = message.getMessageCode(); - mMessageIdentifier = message.getMessageIdentifier(); - mLanguageCode = message.getLanguageCode(); - mMessageBody = message.getMessageBody(); + CellBroadcastMessage(SmsCbMessage message) { + mSmsCbMessage = message; mDeliveryTime = System.currentTimeMillis(); mIsRead = false; } - private CellBroadcastMessage(int geoScope, int serialNumber, - int messageCode, int messageId, String languageCode, - String messageBody, long deliveryTime, boolean isRead) { - mGeographicalScope = geoScope; - mSerialNumber = serialNumber; - mMessageCode = messageCode; - mMessageIdentifier = messageId; - mLanguageCode = languageCode; - mMessageBody = messageBody; + 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. */ public int describeContents() { return 0; } public void writeToParcel(Parcel out, int flags) { - out.writeInt(mGeographicalScope); - out.writeInt(mSerialNumber); - out.writeInt(mMessageCode); - out.writeInt(mMessageIdentifier); - out.writeString(mLanguageCode); - out.writeString(mMessageBody); + mSmsCbMessage.writeToParcel(out, flags); out.writeLong(mDeliveryTime); out.writeInt(mIsRead ? 1 : 0); } @@ -92,10 +85,7 @@ public class CellBroadcastMessage implements Parcelable { public static final Parcelable.Creator<CellBroadcastMessage> CREATOR = new Parcelable.Creator<CellBroadcastMessage>() { public CellBroadcastMessage createFromParcel(Parcel in) { - return new CellBroadcastMessage( - in.readInt(), in.readInt(), - in.readInt(), in.readInt(), in.readString(), - in.readString(), in.readLong(), (in.readInt() != 0)); + return new CellBroadcastMessage(in); } public CellBroadcastMessage[] newArray(int size) { @@ -111,14 +101,75 @@ public class CellBroadcastMessage implements Parcelable { public static CellBroadcastMessage createFromCursor(Cursor cursor) { int geoScope = cursor.getInt(CellBroadcastDatabase.COLUMN_GEOGRAPHICAL_SCOPE); int serialNum = cursor.getInt(CellBroadcastDatabase.COLUMN_SERIAL_NUMBER); - int messageCode = cursor.getInt(CellBroadcastDatabase.COLUMN_MESSAGE_CODE); - int messageId = cursor.getInt(CellBroadcastDatabase.COLUMN_MESSAGE_IDENTIFIER); + int category = cursor.getInt(CellBroadcastDatabase.COLUMN_SERVICE_CATEGORY); String language = cursor.getString(CellBroadcastDatabase.COLUMN_LANGUAGE_CODE); String body = cursor.getString(CellBroadcastDatabase.COLUMN_MESSAGE_BODY); + int format = cursor.getInt(CellBroadcastDatabase.COLUMN_MESSAGE_FORMAT); + int priority = cursor.getInt(CellBroadcastDatabase.COLUMN_MESSAGE_PRIORITY); + + String plmn = null; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_PLMN)) { + plmn = cursor.getString(CellBroadcastDatabase.COLUMN_PLMN); + } + + int lac = -1; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_LAC)) { + lac = cursor.getInt(CellBroadcastDatabase.COLUMN_LAC); + } + + int cid = -1; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_CID)) { + cid = cursor.getInt(CellBroadcastDatabase.COLUMN_CID); + } + + SmsCbLocation location = new SmsCbLocation(plmn, lac, cid); + + SmsCbEtwsInfo etwsInfo = null; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_ETWS_WARNING_TYPE)) { + int warningType = cursor.getInt(CellBroadcastDatabase.COLUMN_ETWS_WARNING_TYPE); + etwsInfo = new SmsCbEtwsInfo(warningType, false, false, null); + } + + SmsCbCmasInfo cmasInfo = null; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_CMAS_MESSAGE_CLASS)) { + int messageClass = cursor.getInt(CellBroadcastDatabase.COLUMN_CMAS_MESSAGE_CLASS); + + int cmasCategory = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_CMAS_CATEGORY)) { + cmasCategory = cursor.getInt(CellBroadcastDatabase.COLUMN_CMAS_CATEGORY); + } + + int responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_CMAS_RESPONSE_TYPE)) { + responseType = cursor.getInt(CellBroadcastDatabase.COLUMN_CMAS_RESPONSE_TYPE); + } + + int severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_CMAS_SEVERITY)) { + severity = cursor.getInt(CellBroadcastDatabase.COLUMN_CMAS_SEVERITY); + } + + int urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_CMAS_URGENCY)) { + urgency = cursor.getInt(CellBroadcastDatabase.COLUMN_CMAS_URGENCY); + } + + int certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + if (!cursor.isNull(CellBroadcastDatabase.COLUMN_CMAS_CERTAINTY)) { + certainty = cursor.getInt(CellBroadcastDatabase.COLUMN_CMAS_CERTAINTY); + } + + cmasInfo = new SmsCbCmasInfo(messageClass, cmasCategory, responseType, severity, + urgency, certainty); + } + + SmsCbMessage msg = new SmsCbMessage(format, geoScope, serialNum, location, category, + language, body, priority, etwsInfo, cmasInfo); + long deliveryTime = cursor.getLong(CellBroadcastDatabase.COLUMN_DELIVERY_TIME); boolean isRead = (cursor.getInt(CellBroadcastDatabase.COLUMN_MESSAGE_READ) != 0); - return new CellBroadcastMessage(geoScope, serialNum, messageCode, messageId, - language, body, deliveryTime, isRead); + + return new CellBroadcastMessage(msg, deliveryTime, isRead); } /** @@ -126,15 +177,43 @@ public class CellBroadcastMessage implements Parcelable { * @return a new ContentValues object containing this object's data */ public ContentValues getContentValues() { - ContentValues cv = new ContentValues(8); - cv.put(CellBroadcastDatabase.Columns.GEOGRAPHICAL_SCOPE, mGeographicalScope); - cv.put(CellBroadcastDatabase.Columns.SERIAL_NUMBER, mSerialNumber); - cv.put(CellBroadcastDatabase.Columns.MESSAGE_CODE, mMessageCode); - cv.put(CellBroadcastDatabase.Columns.MESSAGE_IDENTIFIER, mMessageIdentifier); - cv.put(CellBroadcastDatabase.Columns.LANGUAGE_CODE, mLanguageCode); - cv.put(CellBroadcastDatabase.Columns.MESSAGE_BODY, mMessageBody); + ContentValues cv = new ContentValues(16); + SmsCbMessage msg = mSmsCbMessage; + cv.put(CellBroadcastDatabase.Columns.GEOGRAPHICAL_SCOPE, msg.getGeographicalScope()); + SmsCbLocation location = msg.getLocation(); + if (location.getPlmn() != null) { + cv.put(CellBroadcastDatabase.Columns.PLMN, location.getPlmn()); + } + if (location.getLac() != -1) { + cv.put(CellBroadcastDatabase.Columns.LAC, location.getLac()); + } + if (location.getCid() != -1) { + cv.put(CellBroadcastDatabase.Columns.CID, location.getCid()); + } + cv.put(CellBroadcastDatabase.Columns.SERIAL_NUMBER, msg.getSerialNumber()); + cv.put(CellBroadcastDatabase.Columns.SERVICE_CATEGORY, msg.getServiceCategory()); + cv.put(CellBroadcastDatabase.Columns.LANGUAGE_CODE, msg.getLanguageCode()); + cv.put(CellBroadcastDatabase.Columns.MESSAGE_BODY, msg.getMessageBody()); cv.put(CellBroadcastDatabase.Columns.DELIVERY_TIME, mDeliveryTime); cv.put(CellBroadcastDatabase.Columns.MESSAGE_READ, mIsRead); + cv.put(CellBroadcastDatabase.Columns.MESSAGE_FORMAT, msg.getMessageFormat()); + cv.put(CellBroadcastDatabase.Columns.MESSAGE_PRIORITY, msg.getMessagePriority()); + + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + if (etwsInfo != null) { + cv.put(CellBroadcastDatabase.Columns.ETWS_WARNING_TYPE, etwsInfo.getWarningType()); + } + + SmsCbCmasInfo cmasInfo = mSmsCbMessage.getCmasWarningInfo(); + if (cmasInfo != null) { + cv.put(CellBroadcastDatabase.Columns.CMAS_MESSAGE_CLASS, cmasInfo.getMessageClass()); + cv.put(CellBroadcastDatabase.Columns.CMAS_CATEGORY, cmasInfo.getCategory()); + cv.put(CellBroadcastDatabase.Columns.CMAS_RESPONSE_TYPE, cmasInfo.getResponseType()); + cv.put(CellBroadcastDatabase.Columns.CMAS_SEVERITY, cmasInfo.getSeverity()); + cv.put(CellBroadcastDatabase.Columns.CMAS_URGENCY, cmasInfo.getUrgency()); + cv.put(CellBroadcastDatabase.Columns.CMAS_CERTAINTY, cmasInfo.getCertainty()); + } + return cv; } @@ -146,36 +225,221 @@ public class CellBroadcastMessage implements Parcelable { mIsRead = isRead; } - public int getGeographicalScope() { - return mGeographicalScope; + public String getLanguageCode() { + return mSmsCbMessage.getLanguageCode(); + } + + public int getServiceCategory() { + return mSmsCbMessage.getServiceCategory(); + } + + public long getDeliveryTime() { + return mDeliveryTime; + } + + public String getMessageBody() { + return mSmsCbMessage.getMessageBody(); + } + + public boolean isRead() { + return mIsRead; } public int getSerialNumber() { - return mSerialNumber; + return mSmsCbMessage.getSerialNumber(); } - public int getMessageCode() { - return mMessageCode; + /** + * Returns a styled CharSequence containing the message body and optional CMAS alert headers. + * @param context a Context for resource string access + * @return a CharSequence for display in the broadcast alert dialog + */ + public CharSequence getFormattedMessageBody(Context context) { + if (isCmasMessage()) { + SmsCbCmasInfo cmasInfo = mSmsCbMessage.getCmasWarningInfo(); + SpannableStringBuilder buf = new SpannableStringBuilder(); + + // CMAS category + int categoryId = getCmasCategoryResId(cmasInfo); + if (categoryId != 0) { + buf.append(context.getText(R.string.cmas_category_heading)); + buf.append(context.getText(categoryId)); + buf.append('\n'); + } + + // CMAS response type + int responseId = getCmasResponseResId(cmasInfo); + if (responseId != 0) { + buf.append(context.getText(R.string.cmas_response_heading)); + buf.append(context.getText(responseId)); + buf.append('\n'); + } + + // CMAS severity + int severityId = getCmasSeverityResId(cmasInfo); + if (severityId != 0) { + buf.append(context.getText(R.string.cmas_severity_heading)); + buf.append(context.getText(severityId)); + buf.append('\n'); + } + + // CMAS urgency + int urgencyId = getCmasUrgencyResId(cmasInfo); + if (urgencyId != 0) { + buf.append(context.getText(R.string.cmas_urgency_heading)); + buf.append(context.getText(urgencyId)); + buf.append('\n'); + } + + // CMAS certainty + int certaintyId = getCmasCertaintyResId(cmasInfo); + if (certaintyId != 0) { + buf.append(context.getText(R.string.cmas_certainty_heading)); + buf.append(context.getText(certaintyId)); + buf.append('\n'); + } + + // Style all headings in bold + buf.setSpan(STYLE_BOLD, 0, buf.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + + buf.append(mSmsCbMessage.getMessageBody()); + return buf; + } else { + return mSmsCbMessage.getMessageBody(); + } } - public int getMessageIdentifier() { - return mMessageIdentifier; + /** + * Returns the string resource ID for the CMAS category. + * @return a string resource ID, or 0 if the CMAS category is unknown or not present + */ + private static int getCmasCategoryResId(SmsCbCmasInfo cmasInfo) { + switch (cmasInfo.getCategory()) { + case SmsCbCmasInfo.CMAS_CATEGORY_GEO: + return R.string.cmas_category_geo; + + case SmsCbCmasInfo.CMAS_CATEGORY_MET: + return R.string.cmas_category_met; + + case SmsCbCmasInfo.CMAS_CATEGORY_SAFETY: + return R.string.cmas_category_safety; + + case SmsCbCmasInfo.CMAS_CATEGORY_SECURITY: + return R.string.cmas_category_security; + + case SmsCbCmasInfo.CMAS_CATEGORY_RESCUE: + return R.string.cmas_category_rescue; + + case SmsCbCmasInfo.CMAS_CATEGORY_FIRE: + return R.string.cmas_category_fire; + + case SmsCbCmasInfo.CMAS_CATEGORY_HEALTH: + return R.string.cmas_category_health; + + case SmsCbCmasInfo.CMAS_CATEGORY_ENV: + return R.string.cmas_category_env; + + case SmsCbCmasInfo.CMAS_CATEGORY_TRANSPORT: + return R.string.cmas_category_transport; + + case SmsCbCmasInfo.CMAS_CATEGORY_INFRA: + return R.string.cmas_category_infra; + + case SmsCbCmasInfo.CMAS_CATEGORY_CBRNE: + return R.string.cmas_category_cbrne; + + case SmsCbCmasInfo.CMAS_CATEGORY_OTHER: + return R.string.cmas_category_other; + + default: + return 0; + } } - public String getLanguageCode() { - return mLanguageCode; + /** + * Returns the string resource ID for the CMAS response type. + * @return a string resource ID, or 0 if the CMAS response type is unknown or not present + */ + private static int getCmasResponseResId(SmsCbCmasInfo cmasInfo) { + switch (cmasInfo.getResponseType()) { + case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_SHELTER: + return R.string.cmas_response_shelter; + + case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_EVACUATE: + return R.string.cmas_response_evacuate; + + case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_PREPARE: + return R.string.cmas_response_prepare; + + case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_EXECUTE: + return R.string.cmas_response_execute; + + case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR: + return R.string.cmas_response_monitor; + + case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_AVOID: + return R.string.cmas_response_avoid; + + case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_ASSESS: + return R.string.cmas_response_assess; + + case SmsCbCmasInfo.CMAS_RESPONSE_TYPE_NONE: + return R.string.cmas_response_none; + + default: + return 0; + } } - public long getDeliveryTime() { - return mDeliveryTime; + /** + * Returns the string resource ID for the CMAS severity. + * @return a string resource ID, or 0 if the CMAS severity is unknown or not present + */ + private static int getCmasSeverityResId(SmsCbCmasInfo cmasInfo) { + switch (cmasInfo.getSeverity()) { + case SmsCbCmasInfo.CMAS_SEVERITY_EXTREME: + return R.string.cmas_severity_extreme; + + case SmsCbCmasInfo.CMAS_SEVERITY_SEVERE: + return R.string.cmas_severity_severe; + + default: + return 0; + } } - public String getMessageBody() { - return mMessageBody; + /** + * Returns the string resource ID for the CMAS urgency. + * @return a string resource ID, or 0 if the CMAS urgency is unknown or not present + */ + private static int getCmasUrgencyResId(SmsCbCmasInfo cmasInfo) { + switch (cmasInfo.getUrgency()) { + case SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE: + return R.string.cmas_urgency_immediate; + + case SmsCbCmasInfo.CMAS_URGENCY_EXPECTED: + return R.string.cmas_urgency_expected; + + default: + return 0; + } } - public boolean isRead() { - return mIsRead; + /** + * Returns the string resource ID for the CMAS certainty. + * @return a string resource ID, or 0 if the CMAS certainty is unknown or not present + */ + private static int getCmasCertaintyResId(SmsCbCmasInfo cmasInfo) { + switch (cmasInfo.getCertainty()) { + case SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED: + return R.string.cmas_certainty_observed; + + case SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY: + return R.string.cmas_certainty_likely; + + default: + return 0; + } } /** @@ -188,7 +452,7 @@ public class CellBroadcastMessage implements Parcelable { * @return true if the message is PWS type; false otherwise */ public boolean isPublicAlertMessage() { - return SmsCbHeader.isEmergencyMessage(mMessageIdentifier); + return mSmsCbMessage.isEmergencyMessage(); } /** @@ -198,9 +462,15 @@ public class CellBroadcastMessage implements Parcelable { * @return true if the message is PWS type, excluding Amber alerts */ public boolean isEmergencyAlertMessage() { - int id = mMessageIdentifier; - return SmsCbHeader.isEmergencyMessage(id) && - id != SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY; + if (!mSmsCbMessage.isEmergencyMessage()) { + return false; + } + SmsCbCmasInfo cmasInfo = mSmsCbMessage.getCmasWarningInfo(); + if (cmasInfo != null && + cmasInfo.getMessageClass() == SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY) { + return false; + } + return true; } /** @@ -208,7 +478,7 @@ public class CellBroadcastMessage implements Parcelable { * @return true if the message is ETWS emergency type; false otherwise */ public boolean isEtwsMessage() { - return SmsCbHeader.isEtwsMessage(mMessageIdentifier); + return mSmsCbMessage.isEtwsMessage(); } /** @@ -216,7 +486,20 @@ public class CellBroadcastMessage implements Parcelable { * @return true if the message is CMAS emergency type; false otherwise */ public boolean isCmasMessage() { - return SmsCbHeader.isCmasMessage(mMessageIdentifier); + 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; + } } /** @@ -225,8 +508,8 @@ public class CellBroadcastMessage implements Parcelable { * @return true if the message indicates an ETWS popup alert */ public boolean isEtwsPopupAlert() { - return SmsCbHeader.isEtwsMessage(mMessageIdentifier) && - SmsCbHeader.isEtwsPopupAlert(mMessageCode); + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + return etwsInfo != null && etwsInfo.isPopupAlert(); } /** @@ -235,62 +518,77 @@ public class CellBroadcastMessage implements Parcelable { * @return true if the message indicates an ETWS emergency user alert */ public boolean isEtwsEmergencyUserAlert() { - return SmsCbHeader.isEtwsMessage(mMessageIdentifier) && - SmsCbHeader.isEtwsEmergencyUserAlert(mMessageCode); + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + return etwsInfo != null && etwsInfo.isEmergencyUserAlert(); } - public int getDialogTitleResource() { - switch (mMessageIdentifier) { - case SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING: - return R.string.etws_earthquake_warning; - - case SmsCbConstants.MESSAGE_ID_ETWS_TSUNAMI_WARNING: - return R.string.etws_tsunami_warning; - - case SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_AND_TSUNAMI_WARNING: - return R.string.etws_earthquake_and_tsunami_warning; - - case SmsCbConstants.MESSAGE_ID_ETWS_TEST_MESSAGE: - return R.string.etws_test_message; + /** + * 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; + } - case SmsCbConstants.MESSAGE_ID_ETWS_OTHER_EMERGENCY_TYPE: - return R.string.etws_other_emergency_type; + public int getDialogTitleResource() { + // ETWS warning types + SmsCbEtwsInfo etwsInfo = mSmsCbMessage.getEtwsWarningInfo(); + if (etwsInfo != null) { + switch (etwsInfo.getWarningType()) { + case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE: + return R.string.etws_earthquake_warning; + + case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI: + return R.string.etws_tsunami_warning; + + case SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI: + return R.string.etws_earthquake_and_tsunami_warning; + + case SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE: + return R.string.etws_test_message; + + case SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY: + default: + return R.string.etws_other_emergency_type; + } + } - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL: - return R.string.cmas_presidential_level_alert; + // CMAS warning types + SmsCbCmasInfo cmasInfo = mSmsCbMessage.getCmasWarningInfo(); + if (cmasInfo != null) { + switch (cmasInfo.getMessageClass()) { + case SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT: + return R.string.cmas_presidential_level_alert; - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY: - return R.string.cmas_extreme_alert; + case SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT: + return R.string.cmas_extreme_alert; - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED: - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY: - return R.string.cmas_severe_alert; + case SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT: + return R.string.cmas_severe_alert; - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY: - return R.string.cmas_amber_alert; + case SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY: + return R.string.cmas_amber_alert; - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST: - return R.string.cmas_required_monthly_test; + case SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST: + return R.string.cmas_required_monthly_test; - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE: - return R.string.cmas_exercise_alert; + case SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE: + return R.string.cmas_exercise_alert; - case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE: - return R.string.cmas_operator_defined_alert; + case SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE: + return R.string.cmas_operator_defined_alert; - default: - if (SmsCbHeader.isEmergencyMessage(mMessageIdentifier) || - CellBroadcastConfigService.isOperatorDefinedEmergencyId( - mMessageIdentifier)) { + default: return R.string.pws_other_message_identifiers; - } else { - return R.string.cb_other_message_identifiers; - } + } + } + + if (mSmsCbMessage.isEmergencyMessage()) { + return R.string.pws_other_message_identifiers; + } else { + return R.string.cb_other_message_identifiers; } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiver.java b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiver.java index bce3afde4..164cd8fce 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiver.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiver.java @@ -19,17 +19,21 @@ package com.android.cellbroadcastreceiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.RemoteException; import android.os.ServiceManager; +import android.preference.PreferenceManager; import android.provider.Telephony; import android.telephony.TelephonyManager; +import android.telephony.cdma.CdmaSmsCbProgramData; import android.util.Log; import com.android.internal.telephony.ITelephony; +import com.android.internal.telephony.cdma.sms.SmsEnvelope; public class CellBroadcastReceiver extends BroadcastReceiver { private static final String TAG = "CellBroadcastReceiver"; - static final boolean DBG = true; // TODO: change to false before ship + static final boolean DBG = true; // STOPSHIP: change to false before ship @Override public void onReceive(Context context, Intent intent) { @@ -60,12 +64,93 @@ public class CellBroadcastReceiver extends BroadcastReceiver { } else { Log.e(TAG, "ignoring unprivileged action received " + action); } + } else if (Telephony.Sms.Intents.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION + .equals(action)) { + if (privileged) { + CdmaSmsCbProgramData[] programDataList = (CdmaSmsCbProgramData[]) + intent.getParcelableArrayExtra("program_data_list"); + if (programDataList != null) { + handleCdmaSmsCbProgramData(context, programDataList); + } else { + Log.e(TAG, "SCPD intent received with no program_data_list"); + } + } else { + Log.e(TAG, "ignoring unprivileged action received " + action); + } } else { Log.w(TAG, "onReceive() unexpected action " + action); } } /** + * Handle Service Category Program Data message. + * TODO: Send Service Category Program Results response message to sender + * + * @param context + * @param programDataList + */ + private void handleCdmaSmsCbProgramData(Context context, + CdmaSmsCbProgramData[] programDataList) { + for (CdmaSmsCbProgramData programData : programDataList) { + switch (programData.getOperation()) { + case CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY: + tryCdmaSetCategory(context, programData.getCategory(), true); + break; + + case CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY: + tryCdmaSetCategory(context, programData.getCategory(), false); + break; + + case CdmaSmsCbProgramData.OPERATION_CLEAR_CATEGORIES: + tryCdmaSetCategory(context, + SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, false); + tryCdmaSetCategory(context, + SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT, false); + tryCdmaSetCategory(context, + SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY, false); + tryCdmaSetCategory(context, + SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE, false); + break; + + default: + Log.e(TAG, "Ignoring unknown SCPD operation " + programData.getOperation()); + } + } + } + + private void tryCdmaSetCategory(Context context, int category, boolean enable) { + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + + switch (category) { + case SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT: + sharedPrefs.edit().putBoolean( + CellBroadcastSettings.KEY_ENABLE_CMAS_EXTREME_THREAT_ALERTS, enable) + .apply(); + break; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT: + sharedPrefs.edit().putBoolean( + CellBroadcastSettings.KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS, enable) + .apply(); + break; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY: + sharedPrefs.edit().putBoolean( + CellBroadcastSettings.KEY_ENABLE_CMAS_AMBER_ALERTS, enable).apply(); + break; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE: + sharedPrefs.edit().putBoolean( + CellBroadcastSettings.KEY_ENABLE_CMAS_TEST_ALERTS, enable).apply(); + break; + + default: + Log.w(TAG, "Ignoring SCPD command to " + (enable ? "enable" : "disable") + + " alerts in category " + category); + } + } + + /** * Tell {@link CellBroadcastConfigService} to enable the CB channels. * @param context the broadcast receiver context */ diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java index 6fa593522..9a9b3b7e2 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastReceiverApp.java @@ -17,50 +17,48 @@ package com.android.cellbroadcastreceiver; import android.app.Application; -import android.content.SharedPreferences; import android.util.Log; import android.preference.PreferenceManager; -import static com.android.cellbroadcastreceiver.CellBroadcastReceiver.DBG; +import java.util.concurrent.atomic.AtomicInteger; /** * The application class loads the default preferences at first start, * and remembers the time of the most recently received broadcast. */ public class CellBroadcastReceiverApp extends Application { - public static final String LOG_TAG = "CellBroadcastReceiverApp"; - public static final String PREF_KEY_NOTIFICATION_ID = "notification_id"; - - static CellBroadcastReceiverApp gCellBroadcastReceiverApp; + private static final String LOG_TAG = "CellBroadcastReceiverApp"; @Override public void onCreate() { super.onCreate(); + // TODO: fix strict mode violation from the following method call during app creation PreferenceManager.setDefaultValues(this, R.xml.preferences, false); - gCellBroadcastReceiverApp = this; } - public static CellBroadcastReceiverApp getCellBroadcastReceiverApp() { - return gCellBroadcastReceiverApp; + /** Number of unread non-emergency alerts since the device was booted. */ + private static AtomicInteger sUnreadAlertCount = new AtomicInteger(); + + /** + * Increments the number of unread non-emergency alerts, returning the new value. + * @return the updated number of unread non-emergency alerts, after incrementing + */ + static int incrementUnreadAlertCount() { + return sUnreadAlertCount.incrementAndGet(); } - // Each incoming CB gets its own notification. We have to use a new unique notification id - // for each one. - public int getNextNotificationId() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - int notificationId = prefs.getInt(PREF_KEY_NOTIFICATION_ID, 0); - ++notificationId; - if (notificationId > 32765) { - notificationId = 1; // wrap around before it gets dangerous + /** + * Decrements the number of unread non-emergency alerts after the user reads it. + */ + static void decrementUnreadAlertCount() { + if (sUnreadAlertCount.decrementAndGet() < 0) { + Log.e("CellBroadcastReceiverApp", "mUnreadAlertCount < 0, resetting to 0"); + sUnreadAlertCount.set(0); } + } - // Save the updated notificationId in SharedPreferences - SharedPreferences.Editor editor = prefs.edit(); - editor.putInt(PREF_KEY_NOTIFICATION_ID, notificationId); - editor.apply(); - - if (DBG) Log.d(LOG_TAG, "getNextNotificationId: " + notificationId); - - return notificationId; + /** Resets the unread alert count to zero after user deletes all alerts. */ + static void resetUnreadAlertCount() { + sUnreadAlertCount.set(0); } } diff --git a/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java b/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java index fa5a742c8..6a5d9b06e 100644 --- a/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java +++ b/src/com/android/cellbroadcastreceiver/CellBroadcastSettings.java @@ -21,6 +21,9 @@ import android.os.Bundle; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceActivity; +import android.preference.PreferenceFragment; +import android.preference.PreferenceScreen; +import android.provider.Settings; /** * Settings activity for the cell broadcast receiver. @@ -39,22 +42,26 @@ public class CellBroadcastSettings extends PreferenceActivity { // Speak contents of alert after playing the alert sound. public static final String KEY_ENABLE_ALERT_SPEECH = "enable_alert_speech"; - // Preference category for ETWS related settings. - public static final String KEY_CATEGORY_ETWS_SETTINGS = "category_etws_settings"; - - // Whether to display ETWS test messages (default is disabled). - public static final String KEY_ENABLE_ETWS_TEST_ALERTS = "enable_etws_test_alerts"; - // Preference category for CMAS related settings. public static final String KEY_CATEGORY_CMAS_SETTINGS = "category_cmas_settings"; - // Whether to display CMAS imminent threat notifications (default is enabled). - public static final String KEY_ENABLE_CMAS_IMMINENT_THREAT_ALERTS = - "enable_cmas_imminent_threat_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"; + + // Whether to display CMAS severe threat notifications (default is enabled). + public static final String KEY_ENABLE_CMAS_SEVERE_THREAT_ALERTS = + "enable_cmas_severe_threat_alerts"; - // Whether to display CMAS amber alert messages (default is disabled). + // Whether to display CMAS amber alert messages (default is enabled). public static final String KEY_ENABLE_CMAS_AMBER_ALERTS = "enable_cmas_amber_alerts"; + // Preference category for development settings (enabled by settings developer options toggle). + public static final String KEY_CATEGORY_DEV_SETTINGS = "category_dev_settings"; + + // Whether to display ETWS test messages (default is disabled). + public static final String KEY_ENABLE_ETWS_TEST_ALERTS = "enable_etws_test_alerts"; + // Whether to display CMAS monthly test messages (default is disabled). public static final String KEY_ENABLE_CMAS_TEST_ALERTS = "enable_cmas_test_alerts"; @@ -68,43 +75,75 @@ public class CellBroadcastSettings extends PreferenceActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.preferences); - Resources res = getResources(); - if (!res.getBoolean(R.bool.show_etws_settings)) { - getPreferenceScreen().removePreference(findPreference(KEY_CATEGORY_ETWS_SETTINGS)); - } - if (!res.getBoolean(R.bool.show_cmas_settings)) { - getPreferenceScreen().removePreference(findPreference(KEY_CATEGORY_CMAS_SETTINGS)); - } - if (!res.getBoolean(R.bool.show_brazil_settings)) { - getPreferenceScreen().removePreference(findPreference(KEY_CATEGORY_BRAZIL_SETTINGS)); - } - ListPreference duration = (ListPreference) findPreference(KEY_ALERT_SOUND_DURATION); - duration.setSummary(duration.getEntry()); - duration.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference pref, Object newValue) { - final ListPreference listPref = (ListPreference) pref; - final int idx = listPref.findIndexOfValue((String) newValue); - listPref.setSummary(listPref.getEntries()[idx]); - return true; - } - }); + // Display the fragment as the main content. + getFragmentManager().beginTransaction().replace(android.R.id.content, + new CellBroadcastSettingsFragment()).commit(); + } - Preference.OnPreferenceChangeListener startConfigServiceListener = - new Preference.OnPreferenceChangeListener() { + /** + * New fragment-style implementation of preferences. + */ + public static class CellBroadcastSettingsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.preferences); + + PreferenceScreen preferenceScreen = getPreferenceScreen(); + + // Handler for settings that require us to reconfigure enabled channels in radio + Preference.OnPreferenceChangeListener startConfigServiceListener = + new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference pref, Object newValue) { + CellBroadcastReceiver.startConfigService(pref.getContext()); + return true; + } + }; + + // Only show debug settings when Developer options is enabled in Settings + boolean enableDevSettings = Settings.Secure.getInt(getActivity().getContentResolver(), + Settings.Secure.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; + + if (enableDevSettings) { + // enable/disable all alerts (dev setting) + Preference enablePwsAlerts = findPreference(KEY_ENABLE_EMERGENCY_ALERTS); + if (enablePwsAlerts != null) { + enablePwsAlerts.setOnPreferenceChangeListener(startConfigServiceListener); + } + + // alert sound duration (dev setting) + ListPreference duration = (ListPreference) findPreference(KEY_ALERT_SOUND_DURATION); + duration.setSummary(duration.getEntry()); + duration.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override public boolean onPreferenceChange(Preference pref, Object newValue) { - CellBroadcastReceiver.startConfigService(pref.getContext()); + final ListPreference listPref = (ListPreference) pref; + final int idx = listPref.findIndexOfValue((String) newValue); + listPref.setSummary(listPref.getEntries()[idx]); return true; } - }; - Preference enablePwsAlerts = findPreference(KEY_ENABLE_EMERGENCY_ALERTS); - if (enablePwsAlerts != null) { - enablePwsAlerts.setOnPreferenceChangeListener(startConfigServiceListener); - } - Preference enableChannel50Alerts = findPreference(KEY_ENABLE_CHANNEL_50_ALERTS); - if (enableChannel50Alerts != null) { - enableChannel50Alerts.setOnPreferenceChangeListener(startConfigServiceListener); + }); + } else { + preferenceScreen.removePreference(findPreference(KEY_CATEGORY_DEV_SETTINGS)); + } + + Resources res = getResources(); + if (!res.getBoolean(R.bool.show_cmas_settings)) { + preferenceScreen.removePreference(findPreference(KEY_CATEGORY_CMAS_SETTINGS)); + } + if (!res.getBoolean(R.bool.show_brazil_settings)) { + preferenceScreen.removePreference(findPreference(KEY_CATEGORY_BRAZIL_SETTINGS)); + } + + Preference enableChannel50Alerts = findPreference(KEY_ENABLE_CHANNEL_50_ALERTS); + if (enableChannel50Alerts != null) { + enableChannel50Alerts.setOnPreferenceChangeListener(startConfigServiceListener); + } } } } diff --git a/src/com/android/cellbroadcastreceiver/PrivilegedCellBroadcastReceiver.java b/src/com/android/cellbroadcastreceiver/PrivilegedCellBroadcastReceiver.java index 5ec97c072..2591c59c3 100644 --- a/src/com/android/cellbroadcastreceiver/PrivilegedCellBroadcastReceiver.java +++ b/src/com/android/cellbroadcastreceiver/PrivilegedCellBroadcastReceiver.java @@ -16,7 +16,6 @@ package com.android.cellbroadcastreceiver; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; diff --git a/tests/res/layout/test_buttons.xml b/tests/res/layout/test_buttons.xml index 5ea2ed115..2dd3849c5 100644 --- a/tests/res/layout/test_buttons.xml +++ b/tests/res/layout/test_buttons.xml @@ -25,10 +25,17 @@ android:layout_height="match_parent" android:orientation="vertical"> + <CheckBox android:id="@+id/button_delay_broadcast" + android:text="@string/button_delay_broadcast" + android:layout_marginLeft="20dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <!-- ETWS Alerts --> <Button android:id="@+id/button_etws_normal_type" android:text="@string/button_etws_normal_type" android:layout_marginLeft="20dp" - android:layout_marginTop="20dp" + android:layout_marginTop="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> @@ -44,9 +51,43 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" /> + <!-- CDMA CMAS Alerts --> + <Button android:id="@+id/button_cmas_pres_alert" + android:text="@string/button_cmas_pres_alert" + android:layout_marginLeft="20dp" + android:layout_marginTop="10dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <Button android:id="@+id/button_cmas_extreme_alert" + android:text="@string/button_cmas_extreme_alert" + android:layout_marginLeft="20dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <Button android:id="@+id/button_cmas_severe_alert" + android:text="@string/button_cmas_severe_alert" + android:layout_marginLeft="20dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <Button android:id="@+id/button_cmas_amber_alert" + android:text="@string/button_cmas_amber_alert" + android:layout_marginLeft="20dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <Button android:id="@+id/button_cmas_monthly_test" + android:text="@string/button_cmas_monthly_test" + android:layout_marginLeft="20dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <!-- Other Alerts --> <Button android:id="@+id/button_gsm_7bit_type" android:text="@string/button_gsm_7bit_type" android:layout_marginLeft="20dp" + android:layout_marginTop="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" /> @@ -86,6 +127,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" /> + <Button android:id="@+id/button_gsm_7bit_with_language_body_gsm_type" + android:text="@string/button_gsm_7bit_with_language_body_gsm_type" + android:layout_marginLeft="20dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + <Button android:id="@+id/button_gsm_7bit_with_language_body_umts_type" android:text="@string/button_gsm_7bit_with_language_body_umts_type" android:layout_marginLeft="20dp" @@ -122,12 +169,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" /> - <CheckBox android:id="@+id/button_delay_broadcast" - android:text="@string/button_delay_broadcast" - android:layout_marginLeft="20dp" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> - </LinearLayout> </ScrollView> diff --git a/tests/res/values/strings.xml b/tests/res/values/strings.xml index 16fbb7b35..5968e1c9d 100644 --- a/tests/res/values/strings.xml +++ b/tests/res/values/strings.xml @@ -19,6 +19,11 @@ <string name="button_etws_normal_type">Send ETWS Normal Broadcast</string> <string name="button_etws_cancel_type">Send ETWS Cancel Broadcast</string> <string name="button_etws_test_type">Send ETWS Test Broadcast</string> + <string name="button_cmas_pres_alert">Send CMAS Presidential Alert</string> + <string name="button_cmas_extreme_alert">Send CMAS Extreme Alert</string> + <string name="button_cmas_severe_alert">Send CMAS Severe Alert</string> + <string name="button_cmas_amber_alert">Send CMAS AMBER Alert</string> + <string name="button_cmas_monthly_test">Send CMAS Monthly Test Alert</string> <string name="button_gsm_7bit_type">Send GSM 7 Bit Test Broadcast</string> <string name="button_gsm_7bit_umts_type">Send UMTS 7 Bit Test Broadcast</string> <string name="button_gsm_7bit_nopadding_type">Send GSM 7 Bit Full Length Broadcast</string> @@ -26,6 +31,7 @@ <string name="button_gsm_7bit_multipage_type">Send GSM 7 Bit Multi Page Broadcast</string> <string name="button_gsm_7bit_multipage_umts_type">Send UMTS 7 Bit Multi Page Broadcast</string> <string name="button_gsm_7bit_with_language_type">Send GSM 7 Bit With Language</string> + <string name="button_gsm_7bit_with_language_body_gsm_type">Send GSM 7 Bit With Language In Body</string> <string name="button_gsm_7bit_with_language_body_umts_type">Send UMTS 7 Bit With Language</string> <string name="button_gsm_ucs2_type">Send GSM UCS-2 Test Broadcast</string> <string name="button_gsm_ucs2_umts_type">Send UMTS UCS-2 Test Broadcast</string> diff --git a/tests/src/com/android/cellbroadcastreceiver/DialogSmsDisplayTests.java b/tests/src/com/android/cellbroadcastreceiver/DialogSmsDisplayTests.java index f7df27adf..095ce0628 100644 --- a/tests/src/com/android/cellbroadcastreceiver/DialogSmsDisplayTests.java +++ b/tests/src/com/android/cellbroadcastreceiver/DialogSmsDisplayTests.java @@ -20,12 +20,15 @@ import java.io.UnsupportedEncodingException; import android.content.Intent; import android.provider.Telephony.Sms.Intents; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; import com.android.internal.telephony.EncodeException; import com.android.internal.telephony.GsmAlphabet; import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.gsm.GsmSmsCbMessage; /** * Various instrumentation tests for CellBroadcastReceiver. @@ -106,45 +109,49 @@ public class DialogSmsDisplayTests } } + private static final SmsCbLocation sEmptyLocation = new SmsCbLocation(); + + private static SmsCbMessage createFromPdu(byte[] pdu) { + try { + byte[][] pdus = new byte[1][]; + pdus[0] = pdu; + return GsmSmsCbMessage.createSmsCbMessage(sEmptyLocation, pdus); + } catch (IllegalArgumentException e) { + return null; + } + } + public void testSendMessage7bit() throws Exception { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = encodeCellBroadcast(0, 0, DCS_7BIT_ENGLISH, "Hello in GSM 7 bit"); - intent.putExtra("pdus", pdus); + byte[] pdu = encodeCellBroadcast(0, 0, DCS_7BIT_ENGLISH, "Hello in GSM 7 bit"); + intent.putExtra("message", createFromPdu(pdu)); getActivity().sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public void testSendMessageUCS2() throws Exception { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = encodeCellBroadcast(0, 0, DCS_16BIT_UCS2, "Hello in UCS2"); - intent.putExtra("pdus", pdus); + byte[] pdu = encodeCellBroadcast(0, 0, DCS_16BIT_UCS2, "Hello in UCS2"); + intent.putExtra("message", createFromPdu(pdu)); getActivity().sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public void testSendEtwsMessageNormal() throws Exception { Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = etwsMessageNormal; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(etwsMessageNormal)); getActivity().sendOrderedBroadcast(intent, "android.permission.RECEIVE_EMERGENCY_BROADCAST"); } public void testSendEtwsMessageCancel() throws Exception { Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = etwsMessageCancel; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(etwsMessageCancel)); getActivity().sendOrderedBroadcast(intent, "android.permission.RECEIVE_EMERGENCY_BROADCAST"); } public void testSendEtwsMessageTest() throws Exception { Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = etwsMessageTest; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(etwsMessageTest)); getActivity().sendOrderedBroadcast(intent, "android.permission.RECEIVE_EMERGENCY_BROADCAST"); } diff --git a/tests/src/com/android/cellbroadcastreceiver/tests/SendCdmaCmasMessages.java b/tests/src/com/android/cellbroadcastreceiver/tests/SendCdmaCmasMessages.java new file mode 100644 index 000000000..17f76cb41 --- /dev/null +++ b/tests/src/com/android/cellbroadcastreceiver/tests/SendCdmaCmasMessages.java @@ -0,0 +1,176 @@ +/* + * 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.tests; + +import android.app.Activity; +import android.content.Intent; +import android.os.Parcel; +import android.provider.Telephony; +import android.telephony.SmsCbCmasInfo; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; +import android.test.AndroidTestCase; +import android.util.Log; + +import com.android.internal.telephony.GsmAlphabet; +import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.cdma.sms.BearerData; +import com.android.internal.telephony.cdma.sms.CdmaSmsAddress; +import com.android.internal.telephony.cdma.sms.SmsEnvelope; +import com.android.internal.telephony.cdma.sms.UserData; +import com.android.internal.util.BitwiseOutputStream; + +import java.util.Arrays; +import java.util.Random; + +/** + * Send some test CDMA CMAS warning notifications. + */ +public class SendCdmaCmasMessages { + + private static final String TEST_TEXT = "This is a test CDMA cell broadcast message..." + + "678901234567890123456789012345678901234567890"; + + private static final String PRES_ALERT = + "THE PRESIDENT HAS ISSUED AN EMERGENCY ALERT. CHECK LOCAL MEDIA FOR MORE DETAILS"; + + private static final String EXTREME_ALERT = "FLASH FLOOD WARNING FOR SOUTH COCONINO COUNTY" + + " - NORTH CENTRAL ARIZONA UNTIL 415 PM MST"; + + private static final String SEVERE_ALERT = "SEVERE WEATHER WARNING FOR SOMERSET COUNTY" + + " - NEW JERSEY UNTIL 415 PM MST"; + + private static final String AMBER_ALERT = + "AMBER ALERT:Mountain View,CA VEH'07 Blue Honda Civic CA LIC 5ABC123"; + + private static final String MONTHLY_TEST_ALERT = "This is a test of the emergency alert system." + + " This is only a test. 89012345678901234567890"; + + private static final String IS91_TEXT = "IS91 SHORT MSG"; // max length 14 chars + + public static void testSendCmasPresAlert(Activity activity) { + SmsCbMessage cbMessage = createCmasSmsMessage( + SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT, 12345, "en", + PRES_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_GEO, + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_PREPARE, SmsCbCmasInfo.CMAS_SEVERITY_EXTREME, + SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY); + + Intent intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); + intent.putExtra("message", cbMessage); + activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); + } + + public static void testSendCmasExtremeAlert(Activity activity) { + SmsCbMessage cbMessage = createCmasSmsMessage( + SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, 23456, "en", + EXTREME_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_MET, + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_PREPARE, SmsCbCmasInfo.CMAS_SEVERITY_EXTREME, + SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED); + + Intent intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); + intent.putExtra("message", cbMessage); + activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); + } + + public static void testSendCmasSevereAlert(Activity activity) { + SmsCbMessage cbMessage = createCmasSmsMessage( + SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT, 34567, "en", + SEVERE_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_HEALTH, + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_AVOID, SmsCbCmasInfo.CMAS_SEVERITY_SEVERE, + SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE, SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY); + + Intent intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); + intent.putExtra("message", cbMessage); + activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); + } + + public static void testSendCmasAmberAlert(Activity activity) { + SmsCbMessage cbMessage = createCmasSmsMessage( + SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY, 45678, "en", + AMBER_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, + SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN); + + Intent intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); + intent.putExtra("message", cbMessage); + activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); + } + + public static void testSendCmasMonthlyTest(Activity activity) { + SmsCbMessage cbMessage = createCmasSmsMessage( + SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE, 56789, "en", + MONTHLY_TEST_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, + SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, + SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN); + + Intent intent = new Intent(Telephony.Sms.Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); + intent.putExtra("message", cbMessage); + activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); + } + + /** + * Create a new SmsCbMessage for testing CDMA CMAS support. + * @param serviceCategory the CDMA service category + * @param messageId the 16-bit message identifier + * @param language message language code + * @param body message body + * @param cmasCategory CMAS category (or -1 to skip adding CMAS type 1 elements record) + * @param responseType CMAS response type + * @param severity CMAS severity + * @param urgency CMAS urgency + * @param certainty CMAS certainty + * @return the newly created SmsMessage object + */ + private static SmsCbMessage createCmasSmsMessage(int serviceCategory, int messageId, + String language, String body, int cmasCategory, int responseType, int severity, + int urgency, int certainty) { + int cmasMessageClass = serviceCategoryToCmasMessageClass(serviceCategory); + SmsCbCmasInfo cmasInfo = new SmsCbCmasInfo(cmasMessageClass, cmasCategory, responseType, + severity, urgency, certainty); + return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP2, + SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, messageId, new SmsCbLocation("123456"), + serviceCategory, language, body, SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, null, + cmasInfo); + } + + /** + * Convert CDMA service category to CMAS message class. Copied from {@code BearerData}. + * @param serviceCategory CDMA service category + * @return CMAS message class + */ + private static int serviceCategoryToCmasMessageClass(int serviceCategory) { + switch (serviceCategory) { + case SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT: + return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT: + return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT: + return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY: + return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY; + + case SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE: + return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST; + + default: + return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN; + } + } +} diff --git a/tests/src/com/android/cellbroadcastreceiver/tests/SendTestBroadcastActivity.java b/tests/src/com/android/cellbroadcastreceiver/tests/SendTestBroadcastActivity.java index 7727863e8..0e7310103 100644 --- a/tests/src/com/android/cellbroadcastreceiver/tests/SendTestBroadcastActivity.java +++ b/tests/src/com/android/cellbroadcastreceiver/tests/SendTestBroadcastActivity.java @@ -20,7 +20,6 @@ import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.os.Message; -import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; @@ -102,6 +101,71 @@ public class SendTestBroadcastActivity extends Activity { } }); + /* Send a CMAS presidential alert to app. */ + Button cmasPresAlertButton = (Button) findViewById(R.id.button_cmas_pres_alert); + cmasPresAlertButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mDelayBeforeSending && v != null) { + mPendingButtonClick = this; + mDelayHandler.sendEmptyMessageDelayed(0, DELAY_BEFORE_SENDING_MSEC); + } else { + SendCdmaCmasMessages.testSendCmasPresAlert(SendTestBroadcastActivity.this); + } + } + }); + + /* Send a CMAS extreme alert to app. */ + Button cmasExtremeAlertButton = (Button) findViewById(R.id.button_cmas_extreme_alert); + cmasExtremeAlertButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mDelayBeforeSending && v != null) { + mPendingButtonClick = this; + mDelayHandler.sendEmptyMessageDelayed(0, DELAY_BEFORE_SENDING_MSEC); + } else { + SendCdmaCmasMessages.testSendCmasExtremeAlert(SendTestBroadcastActivity.this); + } + } + }); + + /* Send a CMAS severe alert to app. */ + Button cmasSevereAlertButton = (Button) findViewById(R.id.button_cmas_severe_alert); + cmasSevereAlertButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mDelayBeforeSending && v != null) { + mPendingButtonClick = this; + mDelayHandler.sendEmptyMessageDelayed(0, DELAY_BEFORE_SENDING_MSEC); + } else { + SendCdmaCmasMessages.testSendCmasSevereAlert(SendTestBroadcastActivity.this); + } + } + }); + + /* Send a CMAS AMBER alert to app. */ + Button cmasAmberAlertButton = (Button) findViewById(R.id.button_cmas_amber_alert); + cmasAmberAlertButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mDelayBeforeSending && v != null) { + mPendingButtonClick = this; + mDelayHandler.sendEmptyMessageDelayed(0, DELAY_BEFORE_SENDING_MSEC); + } else { + SendCdmaCmasMessages.testSendCmasAmberAlert(SendTestBroadcastActivity.this); + } + } + }); + + /* Send a CMAS monthly test alert to app. */ + Button cmasMonthlyTestButton = (Button) findViewById(R.id.button_cmas_monthly_test); + cmasMonthlyTestButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mDelayBeforeSending && v != null) { + mPendingButtonClick = this; + mDelayHandler.sendEmptyMessageDelayed(0, DELAY_BEFORE_SENDING_MSEC); + } else { + SendCdmaCmasMessages.testSendCmasMonthlyTest(SendTestBroadcastActivity.this); + } + } + }); + /* Send a GSM 7-bit broadcast message to app. */ Button gsm7bitTypeButton = (Button) findViewById(R.id.button_gsm_7bit_type); gsm7bitTypeButton.setOnClickListener(new OnClickListener() { @@ -197,6 +261,21 @@ public class SendTestBroadcastActivity extends Activity { } }); + /* Send a GSM 7-bit broadcast message with language to app. */ + Button gsm7bitWithLanguageInBodyButton = + (Button) findViewById(R.id.button_gsm_7bit_with_language_body_gsm_type); + gsm7bitWithLanguageInBodyButton.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + if (mDelayBeforeSending && v != null) { + mPendingButtonClick = this; + mDelayHandler.sendEmptyMessageDelayed(0, DELAY_BEFORE_SENDING_MSEC); + } else { + SendTestMessages.testSendMessage7bitWithLanguageInBody( + SendTestBroadcastActivity.this); + } + } + }); + /* Send a UMTS 7-bit broadcast message with language to app. */ Button gsm7bitWithLanguageUmtsButton = (Button) findViewById(R.id.button_gsm_7bit_with_language_body_umts_type); diff --git a/tests/src/com/android/cellbroadcastreceiver/tests/SendTestMessages.java b/tests/src/com/android/cellbroadcastreceiver/tests/SendTestMessages.java index 893d03a3e..a8496ff06 100644 --- a/tests/src/com/android/cellbroadcastreceiver/tests/SendTestMessages.java +++ b/tests/src/com/android/cellbroadcastreceiver/tests/SendTestMessages.java @@ -16,17 +16,19 @@ package com.android.cellbroadcastreceiver.tests; -import java.io.UnsupportedEncodingException; - import android.app.Activity; import android.content.Intent; import android.provider.Telephony.Sms.Intents; -import android.test.ActivityInstrumentationTestCase2; +import android.telephony.SmsCbLocation; +import android.telephony.SmsCbMessage; import android.util.Log; import com.android.internal.telephony.EncodeException; import com.android.internal.telephony.GsmAlphabet; import com.android.internal.telephony.IccUtils; +import com.android.internal.telephony.gsm.GsmSmsCbMessage; + +import java.io.UnsupportedEncodingException; /** * Send test messages. @@ -381,64 +383,47 @@ public class SendTestMessages { (byte)0x50 }; - static byte[] encodeCellBroadcast(int serialNumber, int messageId, int dcs, String message) { - byte[] pdu = new byte[88]; - pdu[0] = (byte) ((serialNumber >> 8) & 0xff); - pdu[1] = (byte) (serialNumber & 0xff); - pdu[2] = (byte) ((messageId >> 8) & 0xff); - pdu[3] = (byte) (messageId & 0xff); - pdu[4] = (byte) (dcs & 0xff); - pdu[5] = 0x11; // single page message + private static final SmsCbLocation sEmptyLocation = new SmsCbLocation(); + + private static SmsCbMessage createFromPdu(byte[] pdu) { try { - byte[] encodedString; - if (dcs == DCS_16BIT_UCS2) { - encodedString = message.getBytes("UTF-16"); - System.arraycopy(encodedString, 0, pdu, 6, encodedString.length); - } else { - // byte 0 of encodedString is the length in septets (don't copy) - encodedString = GsmAlphabet.stringToGsm7BitPacked(message); - System.arraycopy(encodedString, 1, pdu, 6, encodedString.length-1); - } - return pdu; - } catch (EncodeException e) { - Log.e(TAG, "Encode Exception"); + byte[][] pdus = new byte[1][]; + pdus[0] = pdu; + return GsmSmsCbMessage.createSmsCbMessage(sEmptyLocation, pdus); + } catch (IllegalArgumentException e) { return null; - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Unsupported encoding exception for UTF-16"); + } + } + + private static SmsCbMessage createFromPdus(byte[][] pdus) { + try { + return GsmSmsCbMessage.createSmsCbMessage(sEmptyLocation, pdus); + } catch (IllegalArgumentException e) { return null; } } public static void testSendMessage7bit(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsm7BitTest; -// pdus[0] = encodeCellBroadcast(0, 0, DCS_7BIT_ENGLISH, "Hello in GSM 7 bit"); - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsm7BitTest)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessage7bitUmts(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsm7BitTestUmts; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsm7BitTestUmts)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessage7bitNoPadding(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsm7BitTestNoPadding; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsm7BitTestNoPadding)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessage7bitNoPaddingUmts(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsm7BitTestNoPaddingUmts; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsm7BitTestNoPaddingUmts)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } @@ -447,106 +432,81 @@ public class SendTestMessages { byte[][] pdus = new byte[2][]; pdus[0] = gsm7BitTestMultipage1; pdus[1] = gsm7BitTestMultipage2; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdus(pdus)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessage7bitMultipageUmts(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsm7BitTestMultipageUmts; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsm7BitTestMultipageUmts)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessage7bitWithLanguage(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsm7BitTestWithLanguage; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsm7BitTestWithLanguage)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessage7bitWithLanguageInBody(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsm7BitTestWithLanguageInBody; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsm7BitTestWithLanguageInBody)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessage7bitWithLanguageInBodyUmts(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsm7BitTestWithLanguageInBodyUmts; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsm7BitTestWithLanguageInBodyUmts)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessageUcs2(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsmUcs2Test; -// pdus[0] = encodeCellBroadcast(0, 0, DCS_16BIT_UCS2, "Hello in UCS2"); - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsmUcs2Test)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessageUcs2Umts(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsmUcs2TestUmts; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsmUcs2TestUmts)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessageUcs2MultipageUmts(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsmUcs2TestMultipageUmts; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsmUcs2TestMultipageUmts)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessageUcs2WithLanguageInBody(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsmUcs2TestWithLanguageInBody; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsmUcs2TestWithLanguageInBody)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendMessageUcs2WithLanguageUmts(Activity activity) { Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = gsmUcs2TestWithLanguageInBodyUmts; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(gsmUcs2TestWithLanguageInBodyUmts)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_SMS"); } public static void testSendEtwsMessageNormal(Activity activity) { Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = etwsMessageNormal; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(etwsMessageNormal)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_EMERGENCY_BROADCAST"); } public static void testSendEtwsMessageCancel(Activity activity) { Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = etwsMessageCancel; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(etwsMessageCancel)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_EMERGENCY_BROADCAST"); } public static void testSendEtwsMessageTest(Activity activity) { Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION); - byte[][] pdus = new byte[1][]; - pdus[0] = etwsMessageTest; - intent.putExtra("pdus", pdus); + intent.putExtra("message", createFromPdu(etwsMessageTest)); activity.sendOrderedBroadcast(intent, "android.permission.RECEIVE_EMERGENCY_BROADCAST"); } |