欢迎来到 安卓源码空间!
安卓源码空间

                             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;
    }

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

                            版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                        
原文链接:https://blog.csdn.net/qq_38909786/article/details/123291754

copyright@ 2020-2028  安卓源码空间网版权所有   

备案号:豫ICP备2023034476号-1号