Android - 实现SIP通话

        有个项目要求在话机上实现SIP通话,由于以实现系统设置功能部分为主所以在此简单记录下 Sipdroid 的修改部分。

目录

SIP 消息

注册部分

通话部分

        1、通话选项

        2、通话界面


SIP 消息

        查看发送或接收到的消息可以直接在下面列出文件的方法中打印 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)
	 */

通话部分

        1、通话选项

        首先通过 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
...   
     <receiver
            android:name=".ui.Caller"
            android:label="@string/app_name">
            <intent-filter android:priority="-1">
                <action android:name="android.intent.action.NEW_OUTGOING_CALL" />
            </intent-filter>
        </receiver>
...


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<String> vExPats = getTokens(sExPat, ",");
						Vector<String> vPatNums = new Vector<String>();
						Vector<Integer> vTypesCode = new Vector<Integer>();					
				    	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();  
	    				}
	        		}
	            }
	        }
	    }
...

        2、通话界面

          在接通或拨打 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;
    }

        本文仅为根据项目需求所作的简单分析,如有错误欢迎大家指正和交流。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值