/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.cellbroadcastreceiver; import static android.telephony.ServiceState.ROAMING_TYPE_NOT_ROAMING; import android.annotation.NonNull; import android.content.Context; import android.telephony.AccessNetworkConstants; import android.telephony.NetworkRegistrationInfo; import android.telephony.ServiceState; import android.telephony.SmsCbMessage; import android.telephony.TelephonyManager; import android.util.Log; import com.android.cellbroadcastreceiver.CellBroadcastAlertService.AlertType; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * CellBroadcastChannelManager handles the additional cell broadcast channels that * carriers might enable through resources. * Syntax: ":[type=], [emergency=true/false]" * For example, * * "43008:type=earthquake, emergency=true" * "0xAFEE:type=tsunami, emergency=true" * "0xAC00-0xAFED:type=other" * "1234-5678" * * If no tones are specified, the alert type will be set to DEFAULT. If emergency is not set, * by default it's not emergency. */ public class CellBroadcastChannelManager { private static final String TAG = "CBChannelManager"; private static List sCellBroadcastRangeResourceKeys = new ArrayList<>( Arrays.asList(R.array.additional_cbs_channels_strings, R.array.emergency_alerts_channels_range_strings, R.array.cmas_presidential_alerts_channels_range_strings, R.array.cmas_alert_extreme_channels_range_strings, R.array.cmas_alerts_severe_range_strings, R.array.cmas_amber_alerts_channels_range_strings, R.array.required_monthly_test_range_strings, R.array.exercise_alert_range_strings, R.array.operator_defined_alert_range_strings, R.array.etws_alerts_range_strings, R.array.etws_test_alerts_range_strings, R.array.public_safety_messages_channels_range_strings, R.array.state_local_test_alert_range_strings )); private static ArrayList sAllCellBroadcastChannelRanges = null; private static final Object channelRangesLock = new Object(); private final Context mContext; private final int mSubId; /** * Cell broadcast channel range * A range is consisted by starting channel id, ending channel id, and the alert type */ public static class CellBroadcastChannelRange { /** Defines the type of the alert. */ private static final String KEY_TYPE = "type"; /** Defines if the alert is emergency. */ private static final String KEY_EMERGENCY = "emergency"; /** Defines the network RAT for the alert. */ private static final String KEY_RAT = "rat"; /** Defines the scope of the alert. */ private static final String KEY_SCOPE = "scope"; /** Defines the vibration pattern of the alert. */ private static final String KEY_VIBRATION = "vibration"; /** Defines the duration of the alert. */ private static final String KEY_ALERT_DURATION = "alert_duration"; /** Defines if Do Not Disturb should be overridden for this alert */ private static final String KEY_OVERRIDE_DND = "override_dnd"; /** Defines whether writing alert message should exclude from SMS inbox. */ private static final String KEY_EXCLUDE_FROM_SMS_INBOX = "exclude_from_sms_inbox"; /** Define whether to display this cellbroadcast messages. */ private static final String KEY_DISPLAY = "display"; /** Define whether to enable this only in test/debug mode. */ private static final String KEY_TESTING_MODE_ONLY = "testing_mode"; /** Define the channels which not allow opt-out. */ private static final String KEY_ALWAYS_ON = "always_on"; /** Define the duration of screen on in milliseconds. */ private static final String KEY_SCREEN_ON_DURATION = "screen_on_duration"; /** Define whether to display warning icon in the alert dialog. */ private static final String KEY_DISPLAY_ICON = "display_icon"; /** Define whether to dismiss the alert dialog for outside touches */ private static final String KEY_DISMISS_ON_OUTSIDE_TOUCH = "dismiss_on_outside_touch"; /** Define the ISO-639-1 language code associated with the alert message. */ private static final String KEY_LANGUAGE_CODE = "language"; /** * Defines whether the channel needs language filter or not. True indicates that the alert * will only pop-up when the alert's language matches the device's language. */ private static final String KEY_FILTER_LANGUAGE = "filter_language"; public static final int SCOPE_UNKNOWN = 0; public static final int SCOPE_CARRIER = 1; public static final int SCOPE_DOMESTIC = 2; public static final int SCOPE_INTERNATIONAL = 3; public static final int LEVEL_UNKNOWN = 0; public static final int LEVEL_NOT_EMERGENCY = 1; public static final int LEVEL_EMERGENCY = 2; public int mStartId; public int mEndId; public AlertType mAlertType; public int mEmergencyLevel; public int mRanType; public int mScope; public int[] mVibrationPattern; public boolean mFilterLanguage; public boolean mDisplay; public boolean mTestMode; // by default no custom alert duration. play the alert tone with the tone's duration. public int mAlertDuration = -1; public boolean mOverrideDnd = false; // If enable_write_alerts_to_sms_inbox is true, write to sms inbox is enabled by default // for all channels except for channels which explicitly set to exclude from sms inbox. public boolean mWriteToSmsInbox = true; // only set to true for channels not allow opt-out. e.g, presidential alert. public boolean mAlwaysOn = false; // de default screen duration is 1min; public int mScreenOnDuration = 60000; // whether to display warning icon in the pop-up dialog; public boolean mDisplayIcon = true; // whether to dismiss the alert dialog on outside touch. Typically this should be false // to avoid accidental dismisses of emergency messages public boolean mDismissOnOutsideTouch = false; // This is used to override dialog title language public String mLanguageCode; public CellBroadcastChannelRange(Context context, int subId, String channelRange) { mAlertType = AlertType.DEFAULT; mEmergencyLevel = LEVEL_UNKNOWN; mRanType = SmsCbMessage.MESSAGE_FORMAT_3GPP; mScope = SCOPE_UNKNOWN; mVibrationPattern = CellBroadcastSettings.getResources(context, subId) .getIntArray(R.array.default_vibration_pattern); mFilterLanguage = false; // by default all received messages should be displayed. mDisplay = true; mTestMode = false; boolean hasVibrationPattern = false; int colonIndex = channelRange.indexOf(':'); if (colonIndex != -1) { // Parse the alert type and emergency flag String[] pairs = channelRange.substring(colonIndex + 1).trim().split(","); for (String pair : pairs) { pair = pair.trim(); String[] tokens = pair.split("="); if (tokens.length == 2) { String key = tokens[0].trim(); String value = tokens[1].trim(); switch (key) { case KEY_TYPE: mAlertType = AlertType.valueOf(value.toUpperCase()); break; case KEY_EMERGENCY: if (value.equalsIgnoreCase("true")) { mEmergencyLevel = LEVEL_EMERGENCY; } else if (value.equalsIgnoreCase("false")) { mEmergencyLevel = LEVEL_NOT_EMERGENCY; } break; case KEY_RAT: mRanType = value.equalsIgnoreCase("cdma") ? SmsCbMessage.MESSAGE_FORMAT_3GPP2 : SmsCbMessage.MESSAGE_FORMAT_3GPP; break; case KEY_SCOPE: if (value.equalsIgnoreCase("carrier")) { mScope = SCOPE_CARRIER; } else if (value.equalsIgnoreCase("domestic")) { mScope = SCOPE_DOMESTIC; } else if (value.equalsIgnoreCase("international")) { mScope = SCOPE_INTERNATIONAL; } break; case KEY_VIBRATION: String[] vibration = value.split("\\|"); if (vibration.length > 0) { mVibrationPattern = new int[vibration.length]; for (int i = 0; i < vibration.length; i++) { mVibrationPattern[i] = Integer.parseInt(vibration[i]); } hasVibrationPattern = true; } break; case KEY_FILTER_LANGUAGE: if (value.equalsIgnoreCase("true")) { mFilterLanguage = true; } break; case KEY_ALERT_DURATION: mAlertDuration = Integer.parseInt(value); break; case KEY_OVERRIDE_DND: if (value.equalsIgnoreCase("true")) { mOverrideDnd = true; } break; case KEY_EXCLUDE_FROM_SMS_INBOX: if (value.equalsIgnoreCase("true")) { mWriteToSmsInbox = false; } break; case KEY_DISPLAY: if (value.equalsIgnoreCase("false")) { mDisplay = false; } break; case KEY_TESTING_MODE_ONLY: if (value.equalsIgnoreCase("true")) { mTestMode = true; } break; case KEY_ALWAYS_ON: if (value.equalsIgnoreCase("true")) { mAlwaysOn = true; } break; case KEY_SCREEN_ON_DURATION: mScreenOnDuration = Integer.parseInt(value); break; case KEY_DISPLAY_ICON: if (value.equalsIgnoreCase("false")) { mDisplayIcon = false; } break; case KEY_DISMISS_ON_OUTSIDE_TOUCH: if (value.equalsIgnoreCase("true")) { mDismissOnOutsideTouch = true; } break; case KEY_LANGUAGE_CODE: mLanguageCode = value; break; } } } channelRange = channelRange.substring(0, colonIndex).trim(); } // If alert type is info, override vibration pattern if (!hasVibrationPattern && mAlertType.equals(AlertType.INFO)) { mVibrationPattern = CellBroadcastSettings.getResources(context, subId) .getIntArray(R.array.default_notification_vibration_pattern); } // Parse the channel range int dashIndex = channelRange.indexOf('-'); if (dashIndex != -1) { // range that has start id and end id mStartId = Integer.decode(channelRange.substring(0, dashIndex).trim()); mEndId = Integer.decode(channelRange.substring(dashIndex + 1).trim()); } else { // Not a range, only a single id mStartId = mEndId = Integer.decode(channelRange); } } @Override public String toString() { return "Range:[channels=" + mStartId + "-" + mEndId + ",emergency level=" + mEmergencyLevel + ",type=" + mAlertType + ",scope=" + mScope + ",vibration=" + Arrays.toString(mVibrationPattern) + ",alertDuration=" + mAlertDuration + ",filter_language=" + mFilterLanguage + ",override_dnd=" + mOverrideDnd + ",display=" + mDisplay + ",testMode=" + mTestMode + ",mAlwaysOn=" + mAlwaysOn + ",ScreenOnDuration=" + mScreenOnDuration + ", displayIcon=" + mDisplayIcon + "dismissOnOutsideTouch=" + mDismissOnOutsideTouch + ", languageCode=" + mLanguageCode + "]"; } } /** * Constructor * * @param context Context * @param subId Subscription index */ public CellBroadcastChannelManager(Context context, int subId) { mContext = context; mSubId = subId; } /** * Get cell broadcast channels enabled by the carriers from resource key * * @param key Resource key * * @return The list of channel ranges enabled by the carriers. */ public @NonNull ArrayList getCellBroadcastChannelRanges(int key) { ArrayList result = new ArrayList<>(); String[] ranges = CellBroadcastSettings.getResources(mContext, mSubId).getStringArray(key); if (ranges != null) { for (String range : ranges) { try { result.add(new CellBroadcastChannelRange(mContext, mSubId, range)); } catch (Exception e) { loge("Failed to parse \"" + range + "\". e=" + e); } } } return result; } /** * Get all cell broadcast channels * * @return all cell broadcast channels */ public @NonNull ArrayList getAllCellBroadcastChannelRanges() { synchronized(channelRangesLock) { if (sAllCellBroadcastChannelRanges != null) return sAllCellBroadcastChannelRanges; Log.d(TAG, "Create new channel range list"); ArrayList result = new ArrayList<>(); for (int key : sCellBroadcastRangeResourceKeys) { result.addAll(getCellBroadcastChannelRanges(key)); } sAllCellBroadcastChannelRanges = result; return result; } } /** * Clear broadcast channel range list */ public static void clearAllCellBroadcastChannelRanges() { synchronized(channelRangesLock) { if (sAllCellBroadcastChannelRanges != null) { Log.d(TAG, "Clear channel range list"); sAllCellBroadcastChannelRanges = null; } } } /** * @param channel Cell broadcast message channel * @param key Resource key * * @return {@code TRUE} if the input channel is within the channel range defined from resource. * return {@code FALSE} otherwise */ public boolean checkCellBroadcastChannelRange(int channel, int key) { ArrayList ranges = getCellBroadcastChannelRanges(key); for (CellBroadcastChannelRange range : ranges) { if (channel >= range.mStartId && channel <= range.mEndId) { return checkScope(range.mScope); } } return false; } /** * Check if the channel scope matches the current network condition. * * @param rangeScope Range scope. Must be SCOPE_CARRIER, SCOPE_DOMESTIC, or SCOPE_INTERNATIONAL. * @return True if the scope matches the current network roaming condition. */ public boolean checkScope(int rangeScope) { if (rangeScope == CellBroadcastChannelRange.SCOPE_UNKNOWN) return true; TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); tm = tm.createForSubscriptionId(mSubId); ServiceState ss = tm.getServiceState(); if (ss != null) { NetworkRegistrationInfo regInfo = ss.getNetworkRegistrationInfo( NetworkRegistrationInfo.DOMAIN_CS, AccessNetworkConstants.TRANSPORT_TYPE_WWAN); if (regInfo != null) { if (regInfo.getRegistrationState() == NetworkRegistrationInfo.REGISTRATION_STATE_HOME || regInfo.getRegistrationState() == NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING || regInfo.isEmergencyEnabled()) { int voiceRoamingType = regInfo.getRoamingType(); if (voiceRoamingType == ROAMING_TYPE_NOT_ROAMING) { return true; } else if (voiceRoamingType == ServiceState.ROAMING_TYPE_DOMESTIC && rangeScope == CellBroadcastChannelRange.SCOPE_DOMESTIC) { return true; } else if (voiceRoamingType == ServiceState.ROAMING_TYPE_INTERNATIONAL && rangeScope == CellBroadcastChannelRange.SCOPE_INTERNATIONAL) { return true; } return false; } } } // If we can't determine the scope, for safe we should assume it's in. return true; } /** * Return corresponding cellbroadcast range where message belong to * * @param message Cell broadcast message */ public CellBroadcastChannelRange getCellBroadcastChannelRangeFromMessage(SmsCbMessage message) { if (mSubId != message.getSubscriptionId()) { Log.e(TAG, "getCellBroadcastChannelRangeFromMessage: This manager is created for " + "sub " + mSubId + ", should not be used for message from sub " + message.getSubscriptionId()); } int channel = message.getServiceCategory(); ArrayList ranges = null; for (int key : sCellBroadcastRangeResourceKeys) { if (checkCellBroadcastChannelRange(channel, key)) { ranges = getCellBroadcastChannelRanges(key); break; } } if (ranges != null) { for (CellBroadcastChannelRange range : ranges) { if (range.mStartId <= message.getServiceCategory() && range.mEndId >= message.getServiceCategory()) { return range; } } } return null; } /** * Check if the cell broadcast message is an emergency message or not * * @param message Cell broadcast message * @return True if the message is an emergency message, otherwise false. */ public boolean isEmergencyMessage(SmsCbMessage message) { if (message == null) { return false; } if (mSubId != message.getSubscriptionId()) { Log.e(TAG, "This manager is created for sub " + mSubId + ", should not be used for message from sub " + message.getSubscriptionId()); } int id = message.getServiceCategory(); for (int key : sCellBroadcastRangeResourceKeys) { ArrayList ranges = getCellBroadcastChannelRanges(key); for (CellBroadcastChannelRange range : ranges) { if (range.mStartId <= id && range.mEndId >= id) { switch (range.mEmergencyLevel) { case CellBroadcastChannelRange.LEVEL_EMERGENCY: Log.d(TAG, "isEmergencyMessage: true, message id = " + id); return true; case CellBroadcastChannelRange.LEVEL_NOT_EMERGENCY: Log.d(TAG, "isEmergencyMessage: false, message id = " + id); return false; case CellBroadcastChannelRange.LEVEL_UNKNOWN: default: break; } break; } } } Log.d(TAG, "isEmergencyMessage: " + message.isEmergencyMessage() + ", message id = " + id); // If the configuration does not specify whether the alert is emergency or not, use the // emergency property from the message itself, which is checking if the channel is between // MESSAGE_ID_PWS_FIRST_IDENTIFIER (4352) and MESSAGE_ID_PWS_LAST_IDENTIFIER (6399). return message.isEmergencyMessage(); } private static void loge(String msg) { Log.e(TAG, msg); } }