有个项目要求在话机上实现SIP通话,由于以实现系统设置功能部分为主所以在此简单记录下 Sipdroid 的修改部分。
目录
SIP 消息
注册部分
通话部分
1、通话选项
2、通话界面
查看发送或接收到的消息可以直接在下面列出文件的方法中打印 msg 即可,个人认为根据此处的 log 可以方便的查看出各种状态。
org/zoolu/sip/provider/SipProvider.java
...
/** When a new SIP message is received. */
public void onReceivedMessage(Transport transport, Message msg) {
Log.i("lichang", "onReceivedMessage: " + msg);
if (pm == null) {
pm = (PowerManager) Receiver.mContext.getSystemService(Context.POWER_SERVICE);
wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Sipdroid.SipProvider");
}
wl.acquire(); // modified
processReceivedMessage(msg);
wl.release();
}
...
org/zoolu/sip/provider/SipProvider.java
...
/**
* Sends a Message, specifing the transport portocol, nexthop address and
* port.
*/
private ConnectionIdentifier sendMessage(Message msg, String proto,
IpAddress dest_ipaddr, int dest_port, int ttl) {
...
Log.i("lichang", "sendMessage: " + msg);
...
}
...
由于SIP通话仅仅是在话机上扩充的一个功能,因此SIP程序主页面只写了一个登陆状态及配置功能,通过代码分析,Sipdroid 的注册账号是通过 SharedPreferences 读取保存配置的,因此可通过以下代码设置。
SharedPreferences.Editor edit = PreferenceManager.getDefaultSharedPreferences(mContext).edit();
edit.putString(Settings.PREF_SERVER, ""); //代理服务器
edit.putString(Settings.PREF_USERNAME, ""); //用户名
edit.putString(Settings.PREF_DOMAIN, ""); //域名
edit.putString(Settings.PREF_PASSWORD, ""); //密码
edit.putString(Settings.PREF_PORT, ""); //端口
edit.putString(Settings.PREF_FROMUSER, "");
edit.putString(Settings.PREF_PROTOCOL, "udp");
edit.commit();
Receiver.engine(mContext).updateDNS();
Receiver.engine(mContext).halt();
Receiver.engine(mContext).StartEngine();
而且作者在 Settings 中其实直接给出了说明。
sipUA/src/main/java/org/sipdroid/sipua/ui/Settings.java
...
/*-
* ****************************************
* **** HOW TO USE SHARED PREFERENCES *****
* ****************************************
*
* If you need to check the existence of the preference key
* in this class: contains(PREF_USERNAME)
* in other classes: PreferenceManager.getDefaultSharedPreferences(Receiver.mContext).contains(Settings.PREF_USERNAME)
* If you need to check the existence of the key or check the value of the preference
* in this class: getString(PREF_USERNAME, "").equals("")
* in other classes: PreferenceManager.getDefaultSharedPreferences(Receiver.mContext).getString(Settings.PREF_USERNAME, "").equals("")
* If you need to get the value of the preference
* in this class: getString(PREF_USERNAME, DEFAULT_USERNAME)
* in other classes: PreferenceManager.getDefaultSharedPreferences(Receiver.mContext).getString(Settings.PREF_USERNAME, Settings.DEFAULT_USERNAME)
*/
首先通过 Settings 的一些默认配置修改。当然也可由注册中提到的 SharedPreferences 代码实现通话选项的动态修改。
sipUA/src/main/java/org/sipdroid/sipua/ui/Settings.java
...
public static final String DEFAULT_PREF = VAL_PREF_SIP;
//可以修改为以下参数
// All possible values of the PREF_PREF preference (see bellow)
public static final String VAL_PREF_PSTN = "PSTN";
public static final String VAL_PREF_SIP = "SIP";
public static final String VAL_PREF_SIPONLY = "SIPONLY"; //默认SIP
public static final String VAL_PREF_ASK = "ASK"; //询问方式
...
实现部分则是由 Caller 这个广播接收器监听通话广播,然后通过以下代码逻辑判断是否进行 SIP 通话或者普通通话。
sipUA/src/main/AndroidManifest.xml
...
...
sipUA/src/main/java/org/sipdroid/sipua/ui/Caller.java
...
@Override
public void onReceive(final Context context, Intent intent) {
String intentAction = intent.getAction();
String number = getResultData();
Boolean force = false;
if (intentAction.equals(Intent.ACTION_NEW_OUTGOING_CALL) && number != null)
{
/* if (!Receiver.engine(context).isRegistered()) {
Receiver.engine(context).register();
}*/
if (!Sipdroid.release) Log.i("SipUA:","outgoing call");
if (!Sipdroid.on(context)) return;
boolean sip_type = !PreferenceManager.getDefaultSharedPreferences(context).getString(Settings.PREF_PREF, Settings.DEFAULT_PREF).equals(Settings.VAL_PREF_PSTN);
boolean ask = PreferenceManager.getDefaultSharedPreferences(context).getString(Settings.PREF_PREF, Settings.DEFAULT_PREF).equals(Settings.VAL_PREF_ASK);
Log.i("lichang", "onReceive:sip_type= " + sip_type + "\nask=" + ask);
if (Receiver.call_state != UserAgent.UA_STATE_IDLE && RtpStreamReceiver.isBluetoothAvailable()) {
setResultData(null);
switch (Receiver.call_state) {
case UserAgent.UA_STATE_INCOMING_CALL:
Receiver.engine(context).answercall();
if (RtpStreamReceiver.bluetoothmode)
break;
default:
if (RtpStreamReceiver.bluetoothmode)
Receiver.engine(context).rejectcall();
else
Receiver.engine(context).togglebluetooth();
break;
}
return;
}
if (last_number != null && last_number.equals(number) && (SystemClock.elapsedRealtime()-last_time) < 3000) {
setResultData(null);
return;
}
last_time = SystemClock.elapsedRealtime();
last_number = number;
if (number.endsWith("+"))
{
sip_type = !sip_type;
number = number.substring(0,number.length()-1);
force = true;
}
if (SystemClock.elapsedRealtime() < noexclude + 10000) {
noexclude = 0;
force = true;
}
if (sip_type && !force) {
String sExPat = PreferenceManager.getDefaultSharedPreferences(context).getString(Settings.PREF_EXCLUDEPAT, Settings.DEFAULT_EXCLUDEPAT);
boolean bExNums = false;
boolean bExTypes = false;
if (sExPat.length() > 0)
{
Vector vExPats = getTokens(sExPat, ",");
Vector vPatNums = new Vector();
Vector vTypesCode = new Vector();
for(int i = 0; i < vExPats.size(); i++)
{
if (vExPats.get(i).startsWith("h") || vExPats.get(i).startsWith("H"))
vTypesCode.add(Integer.valueOf(ContactsContract.CommonDataKinds.Phone.TYPE_HOME));
else if (vExPats.get(i).startsWith("m") || vExPats.get(i).startsWith("M"))
vTypesCode.add(Integer.valueOf(ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE));
else if (vExPats.get(i).startsWith("w") || vExPats.get(i).startsWith("W"))
vTypesCode.add(Integer.valueOf(ContactsContract.CommonDataKinds.Phone.TYPE_WORK));
else
vPatNums.add(vExPats.get(i));
}
if(vTypesCode.size() > 0)
bExTypes = isExcludedType(vTypesCode, number, context);
if(vPatNums.size() > 0)
bExNums = isExcludedNum(vPatNums, number);
}
if (bExTypes || bExNums)
sip_type = false;
}
if (!sip_type)
{
setResultData(number);
}
else
{
if (number != null && !intent.getBooleanExtra("android.phone.extra.ALREADY_CALLED",false)) {
// Migrate the "prefix" option. TODO Remove this code in a future release.
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
if (sp.contains("prefix")) {
String prefix = sp.getString(Settings.PREF_PREFIX, Settings.DEFAULT_PREFIX);
Editor editor = sp.edit();
if (!prefix.trim().equals("")) {
editor.putString(Settings.PREF_SEARCH, "(.*)," + prefix + "\\1");
}
editor.remove(Settings.PREF_PREFIX);
editor.commit();
}
// Search & replace.
String callthru_number = searchReplaceNumber(context,number);
String callthru_prefix;
if (!ask && !force && PreferenceManager.getDefaultSharedPreferences(context).getBoolean(Settings.PREF_PAR, Settings.DEFAULT_PAR))
{
number = getNumber(context,Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI, number), PhoneLookup._ID);
if (number.equals(""))
number = callthru_number;
} else
number = callthru_number;
if (PreferenceManager.getDefaultSharedPreferences(context).getString(Settings.PREF_PREF, Settings.DEFAULT_PREF).equals(Settings.VAL_PREF_SIPONLY))
force = true;
if (!ask && Receiver.engine(context).call(number,force))
setResultData(null);
else if (!ask && PreferenceManager.getDefaultSharedPreferences(context).getBoolean(Settings.PREF_CALLTHRU, Settings.DEFAULT_CALLTHRU) &&
(callthru_prefix = PreferenceManager.getDefaultSharedPreferences(context).getString(Settings.PREF_CALLTHRU2, Settings.DEFAULT_CALLTHRU2)).length() > 0) {
callthru_number = (callthru_prefix+","+callthru_number+"#");
setResultData(callthru_number);
} else if (ask || force) {
setResultData(null);
final String n = number;
(new Thread() {
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
Intent intent = new Intent(Intent.ACTION_CALL,
Uri.fromParts("sipdroid", Uri.decode(n), null));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}).start();
}
}
}
}
}
...
在接通或拨打 SIP 电话后,会跳转至 InCallScreen 这个活动,所以接下来分析下该活动。首先根据 Activity 的生命周期中的 onCreate() 方法查看一下布局由什么部分组成的,可以看到在加载完布局后会实例化一个 SlidingCardManager(),继续追踪这个实例的代码会在首行看到
Helper class to manage the sliding "call card" on the InCallScreen.
翻译过来就是用来管理InCallScreen上滑动的“调用卡”的助手类。 然后继续查看接下来的代码还会加载一个布局,不过处于初始化过程,此处的视图都是 GONE 的状态。至此,布局全部初始化完成。也就是说,除了 incall 还会加载一个隐藏的 CallCard。
mCallCard.displayOnHoldCallStatus(ccPhone,null);
mCallCard.displayOngoingCallStatus(ccPhone,null);
继续查看 onStart() 方法,发现有一个 Receiver.progress(); 那这里应该就是对于不同通话状态所做的判断了。
public static void progress() {
Log.i("lichang", "progress: 这里应该是通话状态(标题)" + call_state);
if (call_state == UserAgent.UA_STATE_IDLE) return;
int mode = RtpStreamReceiver.speakermode;
if (mode == -1)
mode = speakermode();
if (mode == AudioManager.MODE_NORMAL)
Receiver.onText(Receiver.CALL_NOTIFICATION, mContext.getString(R.string.menu_speaker), android.R.drawable.stat_sys_speakerphone,Receiver.ccCall.base);
else if (bluetooth > 0)
Receiver.onText(Receiver.CALL_NOTIFICATION, mContext.getString(R.string.menu_bluetooth), R.drawable.stat_sys_phone_call_bluetooth,Receiver.ccCall.base);
else switch (call_state) {
case UserAgent.UA_STATE_INCALL:
Receiver.onText(Receiver.CALL_NOTIFICATION, mContext.getString(R.string.card_title_in_progress), R.drawable.stat_sys_phone_call,Receiver.ccCall.base);
break;
case UserAgent.UA_STATE_OUTGOING_CALL:
Receiver.onText(Receiver.CALL_NOTIFICATION, mContext.getString(R.string.card_title_dialing), R.drawable.stat_sys_phone_call,Receiver.ccCall.base);
break;
case UserAgent.UA_STATE_INCOMING_CALL:
Receiver.onText(Receiver.CALL_NOTIFICATION, mContext.getString(R.string.card_title_incoming_call), R.drawable.stat_sys_phone_call,Receiver.ccCall.base);
break;
}
}
由于本次项目需求为用户通过拨打账号,听到提示音后发送 DTMF 音再由服务器转接至呼叫号码,因此流程会稍微改变一下。DTMF 的发送还是由原工程的逻辑实现的,只不过把那些显示隐藏起来了,毕竟话机只能通过按键监听即可实现点击事件。
public void onClick(View v) {
int viewId = v.getId();
// if the button is recognized
if (mDisplayMap.containsKey(viewId)) {
appendDigit(mDisplayMap.get(viewId));
}
}
另外客户要求在通话界面需要可以自行修改通话音量,按键是现有的于是直接添加一下按键监听的代码即可实现。
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
Log.i("lichang", "onKeyUp: " + keyCode + "按键事件" + event);
AudioManager mAudioManager = (AudioManager) Receiver.mContext.getSystemService(
Context.AUDIO_SERVICE);
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
//返回键结束通话
Receiver.engine(mContext).rejectcall();
return true;
case KeyEvent.KEYCODE_DPAD_UP:
mAudioManager.adjustStreamVolume(
AudioManager.STREAM_VOICE_CALL,
AudioManager.ADJUST_RAISE,
AudioManager.FLAG_SHOW_UI);
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
mAudioManager.adjustStreamVolume(
AudioManager.STREAM_VOICE_CALL,
AudioManager.ADJUST_LOWER,
AudioManager.FLAG_SHOW_UI);
break;
}
Receiver.pstn_time = 0;
return false;
}
本文仅为根据项目需求所作的简单分析,如有错误欢迎大家指正和交流。