最常见的紧急号码如119、110,和普通拨号不同的是它们可以在手机不插卡和没有注册运行商网络的的情况下拨出。
号码判断
Android判断号码是否为紧急号码见framework中的PhoneNumberUtils类中isPotentialEmergencyNumber、isEmergencyNumber等方法。
frameworks/base/telephony/java/android/telephony/PhoneNumberUtils.java
/**
* Checks a given number against the list of
* emergency numbers provided by the RIL and SIM card.
*
* @param subId the subscription id of the SIM.
* @param number the number to look up.
* @return true if the number is in the list of emergency numbers
* listed in the RIL / SIM, otherwise return false.
* @hide
*/
public static boolean isEmergencyNumber(int subId, String number) {
// Return true only if the specified number *exactly* matches
// one of the emergency numbers listed by the RIL / SIM.
return isEmergencyNumberInternal(subId, number, true /* useExactMatch */);
}
跳转到isEmergencyNumberInternal,subId是用于决定拨号的phone对象(如果是多卡机器),useExactMatch如字面意思是否进行严格的号码比较,不进行严格比较的话两个号码从尾部开始一定位数的字符相同即认定是同一号码。
private static boolean isEmergencyNumberInternal(int subId, String number,
boolean useExactMatch) {
return isEmergencyNumberInternal(subId, number, null, useExactMatch);
}
调用isEmergencyNumberInternal。
private static boolean isEmergencyNumberInternal(int subId, String number,
String defaultCountryIso,
boolean useExactMatch) {
/// M: @{
Rlog.d(LOG_TAG, "[isEmergencyNumber] number: " + number + ", subId:" + subId);
int PROJECT_SIM_NUM = TelephonyManager.getDefault().getPhoneCount();
boolean needQueryGsm = false;
boolean needQueryCdma = false;
/* It means the caller query ECC number without subId */
if ((subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
|| (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID)) {
...
} else {
return isEmergencyNumberExt(number,
TelephonyManager.getDefault().getCurrentPhoneType(subId));
}
/// @}
调用isEmergencyNumberExt,传入了phone的类型,依据cdma还是gsm走不同的路径。
public static boolean isEmergencyNumberExt(String number, int phoneType) {
...
if (PhoneConstants.PHONE_TYPE_CDMA == phoneType) {
...
} else {
// 1. Check ECCs updated by network
sHashMapForNetworkEccCategory.clear();
String strEccCategoryList = SystemProperties.get("ril.ecc.service.category.list");
if (!TextUtils.isEmpty(strEccCategoryList)) {
for (String strEccCategory : strEccCategoryList.split(";")) {
if (!strEccCategory.isEmpty()) {
String[] strEccCategoryAry = strEccCategory.split(",");
if (2 == strEccCategoryAry.length) {
sHashMapForNetworkEccCategory.put(strEccCategoryAry[0],
Integer.parseInt(strEccCategoryAry[1]));
}
}
}
}
for (String emergencyNum : sHashMapForNetworkEccCategory.keySet()) {
numberPlus = emergencyNum + "+";
if (emergencyNum.equals(number)
|| numberPlus.equals(number)) {
Rlog.d(LOG_TAG, "[isEmergencyNumberExt] match network ecc list");
return true;
}
}
// 2. Check ECCs stored at SIMs
// Read from SIM1
String numbers = SystemProperties.get("ril.ecclist");
Rlog.d(LOG_TAG, "[isEmergencyNumberExt] ril.ecclist: " + numbers);
if (!TextUtils.isEmpty(numbers)) {
// searches through the comma-separated list for a match,
// return true if one is found.
for (String emergencyNum : numbers.split(",")) {
numberPlus = emergencyNum + "+";
if (emergencyNum.equals(number)
|| numberPlus.equals(number)) {
Rlog.d(LOG_TAG, "[isEmergencyNumberExt] match ril.ecclist");
return true;
}
}
bSIMInserted = true;
}
// Read from SIM2
...
// 3. Check ECCs customized by user
if (bSIMInserted) {
if (sCustomizedEccList != null) {
for (EccEntry eccEntry : sCustomizedEccList) {
if (!eccEntry.getCondition().equals(EccEntry.ECC_NO_SIM)) {
String ecc = eccEntry.getEcc();
numberPlus = ecc + "+";
if ((ecc.equals(number) || numberPlus.equals(number))
&& isEccPlmnMatchRegisteredPlmn(eccEntry.getPlmn())) {
Rlog.d(LOG_TAG, "[isEmergencyNumberExt] match"
+ " customized ecc list");
return true;
}
}
}
}
} else {
if (sCustomizedEccList != null) {
for (EccEntry eccEntry : sCustomizedEccList) {
String ecc = eccEntry.getEcc();
numberPlus = ecc + "+";
if (ecc.equals(number)
|| numberPlus.equals(number)) {
Rlog.d(LOG_TAG, "[isEmergencyNumberExt] match"
+ " customized ecc list when no sim");
return true;
}
}
}
}
Rlog.d(LOG_TAG, "[isEmergencyNumberExt] no match");
return false;
}
这里有是三个步骤,首先从系统ril.ecc.service.category.list读取紧急号码列表,这个系统属性的值是号码组成的字符串,用逗号分隔;然后如果没有return的话从再从ril.ecclist系统属性读取号码列表,再做判断;前两步都没有return的话走最后一步读取user设定的号码列表,做最终判断(这个最终判断是mtk订制的代码,mtk特有)。
user列表是从一个xml中读取的,位于mtk源目录代码/vendor/mediatek/proprietary/external/EccList下,名字是ecc_list.xml。
<?xml version="1.0" encoding="utf-8"?>
<EccTable>
<!--
The attribute definition for tag EccEntry:
- Ecc: the emergnecy number
- Category: the service category
- Condition: there are following values:
- 0: ecc only when no sim
- 1: ecc always
- 2: MMI will show ecc but send to nw as normal call
-->
<EccEntry Ecc="112" Category="0" Condition="1" />
<EccEntry Ecc="911" Category="0" Condition="1" />
<EccEntry Ecc="000" Category="0" Condition="0" />
<EccEntry Ecc="08" Category="0" Condition="0" />
<EccEntry Ecc="110" Category="0" Condition="0" />
<EccEntry Ecc="118" Category="0" Condition="0" />
<EccEntry Ecc="119" Category="0" Condition="0" />
<EccEntry Ecc="999" Category="0" Condition="0" />
</EccTable>
Condition为0时在没有插sim卡的情况下判断为紧急号码,1是无条件判断,2是判断为紧急号码但是实际作为普通号码拨出。
parseEccListFromProperty函数用来初始化user列表,PhoneNumberUtils在static块中调用这个初始化函数。
static {
...
parseEccList();
...
parseEccListFromProperty();
...
}
可见想加入或者修改紧急号码列表,在app层面修改ecc_list.xml是最好的选择。ril.ecclist这个会被modem上报的数据填充,所以修改它是不讨巧的。如果是高通代码的话,修改PhoneNumberUtils比修改app省事。
cdma紧急回拨模式
cdma有个特殊的模式,叫做紧急回拨模式,cdma网络拨号后五分钟内是这个模式。这个模式下,紧急呼叫中心可以回拨拨号端,即使拨号手机可能没有插卡或在正常情况下无法注册到网络(例如深山老林、欠费或者sim设置了pin码但是没有解锁)。
packages/services/Telephony/src/com/android/phone/PhoneGlobals.java
phone进程是注册了TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED广播
} else if (action.equals(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED)) {
/// M: For G+C project, only check the foreground phone not enough @{
//if (TelephonyCapabilities.supportsEcm(mCM.getFgPhone())) {
Log.d(LOG_TAG, "Emergency Callback Mode arrived in PhoneApp.");
// Start Emergency Callback Mode service
if (intent.getBooleanExtra("phoneinECMState", false)) {
context.startService(new Intent(context,
EmergencyCallbackModeService.class));
}
在收到广播后启动EmergencyCallbackModeService服务,广播的发送流程由下往上:
frameworks/opt/telephony/src/java/com/android/internal/telephony/cdma/CdmaPhone.java
CdmaPhone.java中的sendEmergencyCallbackModeChange
void sendEmergencyCallbackModeChange(){
//Send an Intent
Intent intent = new Intent(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
intent.putExtra(PhoneConstants.PHONE_IN_ECM_STATE, mIsPhoneInEcmState);
SubscriptionManager.putPhoneIdAndSubIdExtra(intent, getPhoneId());
ActivityManagerNative.broadcastStickyIntent(intent,null,UserHandle.USER_ALL);
if (DBG) Rlog.d(LOG_TAG, "sendEmergencyCallbackModeChange");
}
该函数被handleEnterEmergencyCallbackMode和handleExitEmergencyCallbackMode调用,而这两个函数又是被handleMessage中的EVENT_EMERGENCY_CALLBACK_MODE_ENTER和EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE消息分支调用,
case EVENT_EMERGENCY_CALLBACK_MODE_ENTER:{
handleEnterEmergencyCallbackMode(msg);
}
break;
case EVENT_EXIT_EMERGENCY_CALLBACK_RESPONSE:{
handleExitEmergencyCallbackMode(msg);
}
break;
这两个消息是注册到ril的,ril上报RIL_UNSOL_ENTER_EMERGENCY_CALLBACK_MODE消息时会通知CdmaPhone。
EmergencyCallbackModeService服务的功能主要是发送和取消紧急回拨模式的notification,EmergencyCallbackModeExitDialog是该模式退出时候现实的对话框类
流程很简单。
紧急拨号重试
重试机制针对两种情况:
1.机器没有注册网络,例如目前处于飞行模式,紧急拨号时会弹出提示框,点击确定后关闭飞行模式然后自动重新拨号
2.多卡机器切换拨号卡槽重试,很多平台是双卡的,但是一般都是卡槽1可以做到4G或者全网通,卡槽2只能上2G网络。还有就是全网通的机器一般只能用卡槽1上cdma网络,卡槽2不能。当然有的平台是可以切换4G卡槽的,卡槽2也可以上4G,但是切换到卡槽2的时候卡槽1就只能上2G网络了。2G网络拨不通紧急号码的情况下,会切换到另一个卡槽重试。
packages/services/Telephony/src/com/android/services/telephony/EmergencyCallHelper.java
重试的代码都在EmergencyCallHelper中。实现逻辑也很简单。利用handler控制流程,因为有sendEmptyMessageDelayed方法,可以控制一段代码延时执行。
例如针对上述情况1的代码:
private void startSequenceInternal(Phone phone, Callback callback) {
Log.d(this, "startSequenceInternal()");
// First of all, clean up any state left over from a prior emergency call sequence. This
// ensures that we'll behave sanely if another startTurnOnRadioSequence() comes in while
// we're already in the middle of the sequence.
cleanup();
mPhone = phone;
mCallback = callback;
// No need to check the current service state here, since the only reason to invoke this
// method in the first place is if the radio is powered-off. So just go ahead and turn the
// radio on.
powerOnRadio(); // We'll get an onServiceStateChanged() callback
// when the radio successfully comes up.
// Next step: when the SERVICE_STATE_CHANGED event comes in, we'll retry the call; see
// onServiceStateChanged(). But also, just in case, start a timer to make sure we'll retry
// the call even if the SERVICE_STATE_CHANGED event never comes in for some reason.
startRetryTimer();
}
先用powerOnRadio开启关闭飞行模式,然后startRetryTimer发送延时拨号代码mCallBack
private void startRetryTimer() {
cancelRetryTimer();
mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS);
}
在 MSG_RETRY_TIMEOUT消息处理中调用mCallBack接口,该接口定义就一个函数onComplete
private void onComplete(boolean isRadioReady) {
if (mCallback != null) {
Callback tempCallback = mCallback;
mCallback = null;
tempCallback.onComplete(isRadioReady);
}
}
mEmergencyCallHelper.startTurnOnRadioSequence(eccPhone,
new EmergencyCallHelper.Callback() {
@Override
public void onComplete(boolean isRadioReady) {
...
placeOutgoingConnection(connection, eccPhone, request);
...
}
});
情况2的流程类似,不再赘述
重试机制当然还可以做到modem代码层次,mtk的2/3G网络自动切换代码就是在该层的,不在app层处理。所以拨号的时候可能看到状态栏信号处显示掉网然后马上又注册上网络这个是正常现象。
紧急拨号盘
该拨号盘的入口在解锁UI处,有紧急呼叫入口按键。
packages/services/Telephony/src/com/android/phone/EmergencyDialer.javapublic class EmergencyDialer extends Activity implements View.OnClickListener,
View.OnLongClickListener, View.OnKeyListener, TextWatcher, DialpadKeyButton.OnPressedListener {
是个Activity,UI结构和逻辑都比较简单,下方是12key按键输入区和拨号按键,上方有个EditText显示所输入的号码。
// Allow this activity to be displayed in front of the keyguard / lockscreen.
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
在OnCreate中使用的解锁的FLAG,所以可以在锁屏之上显示。
紧急拨号处理流程
按照目前的规范,手机终端所做的工作其实并不多,最终紧急拨号呼出后能正常拨通到相应部门其实大部分功能是运营商所要做的,紧急拨号呼出后,会接到运营商紧急拨号中心,紧急拨号中心分配到号码接入何处(例如医院,或者消防队等),但是目前这个流程可以说是一片混乱:
1.手机终端在不插卡紧急拨号时会寻找所在位置信号最好的网络注册,例如全网通机器移动、联通或者电信网络都是可以注册的,注册的网络可以是2/3/4G。不同运营商,同运营商的不同网络,同一运营商同一制式网络不同地区(例如深圳和东莞),目前依据测试结果看效果是多样的,如果注册到深圳联通网络是拨不通的,注册到移动是可以拨通的,所以会出现同一手机不同时候有的时候能拨通有的时候拨不通。
2.插卡情况,联通的机器在深圳2G网络下是拨不通的,但是插卡后在2G网络下作为普通号码拨出后是可以拨通的。这个就是mtk所加代码的用处,可以把紧急号码在插卡的时候作为普通号码拨出,规避了一些情况。mtk针对紧急拨号是有专用的AT命令的,高通没有(估计是modem代码处理,framework和app不处理,上层可以通过设置系统属性控制是否按普通号码拨出)。
3.手机在3G/4G的情况下接通情况明显比2G下要好得多,所以mtk加入了2G拨不通后3G重试的机制。但是这会引起网络的重新注册,状态栏会显示掉网再注册,然后有人就说这个是bug,很无语。
4.不插卡或插卡2G网络可以接通,但是只会听到一段音频不停循环播放(火警请拨119,匪警请拨110...)。这个只能插卡切换到3/4G网络去拨了....
其实根本就是紧急拨号中心没搞好啊,这个运营商没有利益驱动不可能解决目前这种状态的。