Explorar o código

sip版本切换和优化

weizhengliang hai 1 ano
pai
achega
d404010ce0
Modificáronse 24 ficheiros con 2087 adicións e 531 borrados
  1. 34 0
      android_door/src/main/assets/assistant_default_values
  2. 40 0
      android_door/src/main/assets/assistant_linphone_default_values
  3. 42 0
      android_door/src/main/assets/linphonerc_default
  4. 43 0
      android_door/src/main/assets/linphonerc_factory
  5. 11 21
      android_door/src/main/common/java/com/wdkl/ncs/host/activity/CallActivity.java
  6. 7 12
      android_door/src/main/common/java/com/wdkl/ncs/host/activity/SipTestActivity.kt
  7. 0 332
      android_door/src/main/common/java/com/wdkl/ncs/host/service/WdklSipService.java
  8. 45 0
      android_door/src/main/common/java/com/wdkl/ncs/host/sip/callback/PhoneCallback.java
  9. 13 0
      android_door/src/main/common/java/com/wdkl/ncs/host/sip/callback/RegistrationCallback.java
  10. 540 0
      android_door/src/main/common/java/com/wdkl/ncs/host/sip/core/CorePreferences.kt
  11. 248 0
      android_door/src/main/common/java/com/wdkl/ncs/host/sip/core/LinCoreService.java
  12. 521 0
      android_door/src/main/common/java/com/wdkl/ncs/host/sip/core/LinphoneManager.kt
  13. 23 22
      android_door/src/main/common/java/com/wdkl/ncs/host/util/AudioRouteUtils.kt
  14. 171 0
      android_door/src/main/common/java/com/wdkl/ncs/host/sip/utils/FileUtils.kt
  15. 106 0
      android_door/src/main/common/java/com/wdkl/ncs/host/sip/utils/LinphoneUtils.kt
  16. 148 0
      android_door/src/main/common/java/com/wdkl/ncs/host/sip/utils/VideoZoomHelper.kt
  17. 0 20
      android_door/src/main/common/res/raw/linphonerc_default
  18. 0 34
      android_door/src/main/common/res/raw/linphonerc_factory
  19. 7 3
      android_door/src/main/h10_3128/AndroidManifest.xml
  20. 71 44
      android_door/src/main/h10_3128/java/com/wdkl/app/ncs/callingdoor/activity/CallingdoorActivity.kt
  21. 16 35
      android_door/src/main/h10_3128/java/com/wdkl/app/ncs/callingdoor/fragment/SipCallFragment.kt
  22. 0 7
      android_door/src/main/h10_3128/java/com/wdkl/app/ncs/callingdoor/helper/AppUpdateHelper.java
  23. 1 1
      common/build.gradle
  24. BIN=BIN
      common/libs/linphone-sdk-android-5.2.10.aar

+ 34 - 0
android_door/src/main/assets/assistant_default_values

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
+  <section name="proxy_default_values">
+    <entry name="avpf" overwrite="true">0</entry>
+    <entry name="dial_escape_plus" overwrite="true">0</entry>
+    <entry name="publish" overwrite="true">0</entry>
+    <entry name="quality_reporting_collector" overwrite="true"></entry>
+    <entry name="quality_reporting_enabled" overwrite="true">0</entry>
+    <entry name="quality_reporting_interval" overwrite="true">0</entry>
+    <entry name="reg_expires" overwrite="true">3600</entry>
+    <entry name="reg_identity" overwrite="true"></entry>
+    <entry name="reg_proxy" overwrite="true"></entry>
+    <entry name="reg_route" overwrite="true"></entry>
+    <entry name="reg_sendregister" overwrite="true">1</entry>
+    <entry name="nat_policy_ref" overwrite="true"></entry>
+    <entry name="realm" overwrite="true"></entry>
+    <entry name="conference_factory_uri" overwrite="true"></entry>
+    <entry name="push_notification_allowed" overwrite="true">0</entry>
+  </section>
+  <section name="nat_policy_default_values">
+    <entry name="stun_server" overwrite="true"></entry>
+    <entry name="protocols" overwrite="true"></entry>
+  </section>
+  <section name="assistant">
+    <entry name="domain" overwrite="true"></entry>
+    <entry name="algorithm" overwrite="true">MD5</entry>
+    <entry name="password_max_length" overwrite="true">-1</entry>
+    <entry name="password_min_length" overwrite="true">0</entry>
+    <entry name="username_length" overwrite="true">-1</entry>
+    <entry name="username_max_length" overwrite="true">128</entry>
+    <entry name="username_min_length" overwrite="true">1</entry>
+    <entry name="username_regex" overwrite="true">^[a-zA-Z0-9+_.\-]*$</entry>
+  </section>
+</config>

+ 40 - 0
android_door/src/main/assets/assistant_linphone_default_values

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
+  <section name="proxy_default_values">
+    <entry name="avpf" overwrite="true">1</entry>
+    <entry name="dial_escape_plus" overwrite="true">0</entry>
+    <entry name="publish" overwrite="true">0</entry>
+    <entry name="quality_reporting_collector" overwrite="true">sip:voip-metrics@sip.linphone.org;transport=tls</entry>
+    <entry name="quality_reporting_enabled" overwrite="true">1</entry>
+    <entry name="quality_reporting_interval" overwrite="true">180</entry>
+    <entry name="reg_expires" overwrite="true">31536000</entry>
+    <entry name="reg_identity" overwrite="true">sip:?@sip.linphone.org</entry>
+    <entry name="reg_proxy" overwrite="true">&lt;sip:sip.linphone.org;transport=tls&gt;</entry>
+    <entry name="reg_route" overwrite="true">&lt;sip:sip.linphone.org;transport=tls&gt;</entry>
+    <entry name="reg_sendregister" overwrite="true">1</entry>
+    <entry name="nat_policy_ref" overwrite="true">nat_policy_default_values</entry>
+    <entry name="realm" overwrite="true">sip.linphone.org</entry>
+    <entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
+    <entry name="push_notification_allowed" overwrite="true">1</entry>
+  </section>
+  <section name="nat_policy_default_values">
+    <entry name="stun_server" overwrite="true">stun.linphone.org</entry>
+    <entry name="protocols" overwrite="true">stun,ice</entry>
+  </section>
+  <section name="sip">
+    <entry name="rls_uri" overwrite="true">sips:rls@sip.linphone.org</entry>
+  </section>
+  <section name="lime">
+    <entry name="x3dh_server_url" overwrite="true">https://lime.linphone.org/lime-server/lime-server.php</entry>
+  </section>
+  <section name="assistant">
+    <entry name="domain" overwrite="true">sip.linphone.org</entry>
+    <entry name="algorithm" overwrite="true">SHA-256</entry>
+    <entry name="password_max_length" overwrite="true">-1</entry>
+    <entry name="password_min_length" overwrite="true">1</entry>
+    <entry name="username_length" overwrite="true">-1</entry>
+    <entry name="username_max_length" overwrite="true">64</entry>
+    <entry name="username_min_length" overwrite="true">1</entry>
+    <entry name="username_regex" overwrite="true">^[a-z0-9+_.\-]*$</entry>
+  </section>
+</config>

+ 42 - 0
android_door/src/main/assets/linphonerc_default

@@ -0,0 +1,42 @@
+
+## Start of default rc
+
+[sip]
+contact="Linphone Android" <sip:linphone.android@unknown-host>
+use_info=0
+use_ipv6=1
+keepalive_period=30000
+sip_port=-1
+sip_tcp_port=-1
+sip_tls_port=-1
+media_encryption=none
+
+[net]
+#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"
+download_bw=0
+upload_bw=0
+
+[video]
+size=vga
+
+[app]
+tunnel=disabled
+push_notification=1
+auto_start=1
+
+[tunnel]
+host=
+port=443
+
+[misc]
+log_collection_upload_server_url=https://www.linphone.org:444/lft.php
+file_transfer_server_url=https://www.linphone.org:444/lft.php
+version_check_url_root=https://www.linphone.org/releases
+max_calls=10
+history_max_size=100
+
+[in-app-purchase]
+server_url=https://subscribe.linphone.org:444/inapp.php
+purchasable_items_ids=test_account_subscription
+
+## End of default rc

+ 43 - 0
android_door/src/main/assets/linphonerc_factory

@@ -0,0 +1,43 @@
+
+## Start of factory rc
+
+# This file shall not contain path referencing package name, in order to be portable when app is renamed.
+# Paths to resources must be set from LinphoneManager, after creating LinphoneCore.
+
+[net]
+mtu=1300
+force_ice_disablement=0
+
+[sip]
+guess_hostname=1
+register_only_when_network_is_up=1
+auto_net_state_mon=1
+auto_answer_replacing_calls=1
+ping_with_options=0
+use_cpim=1
+
+[sound]
+#remove this property for any application that is not Linphone public version itself
+ec_calibrator_cool_tones=1
+
+[video]
+displaytype=MSAndroidTextureDisplay
+auto_resize_preview_to_keep_ratio=1
+
+[misc]
+enable_basic_to_client_group_chat_room_migration=0
+enable_simple_group_chat_message_state=0
+aggregate_imdn=1
+notify_each_friend_individually_when_presence_received=0
+
+[app]
+activation_code_length=4
+prefer_basic_chat_room=1
+
+[assistant]
+xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
+
+[lime]
+lime_update_threshold=-1
+
+## End of factory rc

+ 11 - 21
android_door/src/main/common/java/com/wdkl/ncs/host/activity/CallActivity.java

@@ -16,8 +16,8 @@ import android.widget.RelativeLayout;
 import androidx.annotation.Nullable;
 
 import com.wdkl.app.ncs.callingdoor.R;
-import com.wdkl.ncs.host.service.WdklSipService;
-import com.wdkl.ncs.host.util.AudioRouteUtils;
+import com.wdkl.ncs.android.lib.base.BaseApplication;
+import com.wdkl.ncs.host.sip.core.LinphoneManager;
 
 import org.linphone.core.Call;
 import org.linphone.core.Core;
@@ -35,7 +35,8 @@ public class CallActivity extends Activity {
     private CoreListenerStub mCoreListener;
 
     private AudioManager audioManager;
-    private Core core;
+
+    private LinphoneManager linphoneManager;
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -46,10 +47,8 @@ public class CallActivity extends Activity {
         mVideoView = findViewById(R.id.videoSurface);
         mCaptureView = findViewById(R.id.videoCaptureSurface);
 
-        core = WdklSipService.getCore();
-        // 配置核心视频层渲染
-        core.setNativeVideoWindowId(mVideoView);
-        core.setNativePreviewWindowId(mCaptureView);
+        linphoneManager = LinphoneManager.Companion.getInstance(BaseApplication.appContext);
+        linphoneManager.setVideoWindowId(mVideoView, mCaptureView);
 
         audioManager = (AudioManager) getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
 
@@ -73,14 +72,7 @@ public class CallActivity extends Activity {
         findViewById(R.id.terminate_call).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                Core core = WdklSipService.getCore();
-                if (core.getCallsNb() > 0) {
-                    Call call = core.getCurrentCall();
-                    if (call == null) {
-                        call = core.getCalls()[0];
-                    }
-                    call.terminate();
-                }
+                linphoneManager.terminateCall();
             }
         });
     }
@@ -98,9 +90,7 @@ public class CallActivity extends Activity {
         audioManager.setSpeakerphoneOn(true);
         Log.i(TAG,">>>>>>>>>>>>>>"+audioManager.isSpeakerphoneOn());
 
-        if (core != null) {
-            AudioRouteUtils.Companion.routeAudioToSpeaker(core, null, false);
-        }
+        linphoneManager.enableSpeaker(true);
     }
 
     @Override
@@ -112,13 +102,13 @@ public class CallActivity extends Activity {
     protected void onResume() {
         super.onResume();
 
-        WdklSipService.getCore().addListener(mCoreListener);
+        linphoneManager.getCore().addListener(mCoreListener);
         resizePreview();
     }
 
     @Override
     protected void onPause() {
-        WdklSipService.getCore().removeListener(mCoreListener);
+        linphoneManager.getCore().removeListener(mCoreListener);
 
         super.onPause();
     }
@@ -153,7 +143,7 @@ public class CallActivity extends Activity {
     }
 
     private void resizePreview() {
-        Core core = WdklSipService.getCore();
+        Core core = linphoneManager.getCore();
         if (core.getCallsNb() > 0) {
             Call call = core.getCurrentCall();
             if (call == null) {

+ 7 - 12
android_door/src/main/common/java/com/wdkl/ncs/host/activity/SipTestActivity.kt

@@ -4,19 +4,19 @@ import android.os.Bundle
 import android.widget.Toast
 import androidx.appcompat.app.AppCompatActivity
 import com.wdkl.app.ncs.callingdoor.R
-import com.wdkl.ncs.host.service.WdklSipService
+import com.wdkl.ncs.android.lib.base.BaseApplication
+import com.wdkl.ncs.host.sip.core.LinphoneManager
 import kotlinx.android.synthetic.main.activity_sip_test.*
-import org.linphone.core.Core
 
 class SipTestActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_sip_test)
 
-        val core: Core = WdklSipService.getCore()
-        WdklSipService.sipTesting = true
+        val linphoneManager = LinphoneManager.getInstance(BaseApplication.appContext)
+        LinphoneManager.sipTesting = true
 
-        sip_test_id.text = core.identity
+        sip_test_id.text = linphoneManager.getCore().identity
 
         sip_test_back.setOnClickListener {
             finish()
@@ -29,17 +29,12 @@ class SipTestActivity : AppCompatActivity() {
                 return@setOnClickListener
             }
 
-            val addressToCall = core.interpretUrl(sipNo.toString())
-            val params = core.createCallParams(null)
-            params?.isVideoEnabled  = false
-            if (addressToCall != null) {
-                core.inviteAddressWithParams(addressToCall, params!!)
-            }
+            linphoneManager.startCall(sipNo.toString(), false)
         }
     }
 
     override fun finish() {
-        WdklSipService.sipTesting = false
+        LinphoneManager.sipTesting = false
         super.finish()
     }
 }

+ 0 - 332
android_door/src/main/common/java/com/wdkl/ncs/host/service/WdklSipService.java

@@ -1,332 +0,0 @@
-package com.wdkl.ncs.host.service;
-
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.Handler;
-import android.os.IBinder;
-import android.util.Log;
-import android.widget.Toast;
-
-import androidx.annotation.Nullable;
-
-import com.wdkl.app.ncs.callingdoor.R;
-import com.wdkl.ncs.android.lib.base.BaseApplication;
-import com.wdkl.ncs.android.lib.settings.SettingConfig;
-import com.wdkl.ncs.android.middleware.common.Constant;
-import com.wdkl.ncs.android.middleware.common.MessageEvent;
-import com.wdkl.ncs.host.activity.CallActivity;
-
-import org.greenrobot.eventbus.EventBus;
-import org.linphone.core.Call;
-import org.linphone.core.CallParams;
-import org.linphone.core.Core;
-import org.linphone.core.CoreListenerStub;
-import org.linphone.core.Factory;
-import org.linphone.core.LogCollectionState;
-import org.linphone.core.PayloadType;
-import org.linphone.core.ProxyConfig;
-import org.linphone.core.RegistrationState;
-import org.linphone.mediastream.Version;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Timer;
-import java.util.TimerTask;
-
-public class WdklSipService extends Service {
-    private static final String START_SIPPHONE_LOGS = " ==== Device information dump ====";
-
-    private static final String PREFER_PAYLOAD = "PUMU";
-
-    //单例化服务,以便全局调用
-    private static WdklSipService sInstance;
-
-
-    private NotificationManager notificationManager = null;
-    private String notificationId = "channelId0";
-    private String notificationName = "sip_service";
-
-    private Handler mHandler;
-    private Timer mTimer;
-
-    private Core mCore;
-    private CoreListenerStub mCoreListener;
-
-    public static boolean sipTesting = false;
-
-    public static boolean isReady() {
-        return sInstance != null;
-    }
-
-    public static WdklSipService getInstance() {
-        return sInstance;
-    }
-
-    public static Core getCore() {
-        return sInstance.mCore;
-    }
-
-    @Nullable
-    @Override
-    public IBinder onBind(Intent intent) {
-        return null;
-    }
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-
-        notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
-
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            NotificationChannel channel = new NotificationChannel(
-                    notificationId,
-                    notificationName,
-                    NotificationManager.IMPORTANCE_HIGH
-            );
-            channel.enableVibration(true);
-            channel.enableLights(true);
-            channel.setBypassDnd(true);
-            channel.setShowBadge(true);
-            channel.setSound(null, null);
-            notificationManager.createNotificationChannel(channel);
-        }
-
-        //首次调用必须使用 Factory相关方法
-        //这里开户调试日志及设置路径
-        String basePath = getFilesDir().getAbsolutePath();
-        Factory.instance().setLogCollectionPath(basePath);
-        Factory.instance().enableLogCollection(LogCollectionState.Enabled);
-        Factory.instance().setDebugMode(false, getString(R.string.javashop_app_name));
-
-        //收集一些设备信息
-        Log.i("sipCall", START_SIPPHONE_LOGS);
-        dumpDeviceInformation();
-        dumpInstalledLinphoneInformation();
-
-        mHandler = new Handler();
-        //主监听器,根据事件调用界面
-        mCoreListener = new CoreListenerStub() {
-            @Override
-            public void onCallStateChanged(Core core, Call call, Call.State state, String message) {
-                if (!SettingConfig.getSipEnabled(BaseApplication.appContext)) {
-                    return;
-                }
-
-                Toast.makeText(WdklSipService.this, message, Toast.LENGTH_SHORT).show();
-                Log.d("sipCall", ">>>>>>>>>>>> call state: " + state + ", " + call.getRemoteAddress().asString());
-
-                if (state == Call.State.IncomingReceived || state == Call.State.IncomingEarlyMedia) {
-                    //Toast.makeText(WdklSipService.this, "Incoming call", Toast.LENGTH_LONG).show();
-                    //来电时将自动接听
-                    CallParams params = getCore().createCallParams(call);
-                    call.acceptWithParams(params);
-                } else if (state == Call.State.Connected) {
-                    if (sipTesting) {
-                        //通话已建立完成,打开通话界面
-                        Intent intent = new Intent(WdklSipService.this, CallActivity.class);
-                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-                        startActivity(intent);
-                    } else {
-                        EventBus.getDefault().post(new MessageEvent("sip_connected", Constant.SIP_CONNECTED));
-                    }
-                } else if (state == Call.State.End || state == Call.State.Released){
-                    EventBus.getDefault().post(new MessageEvent("handoff", Constant.EVENT_END_CALL));
-                }
-            }
-
-            @Override
-            public void onRegistrationStateChanged(Core core, ProxyConfig cfg, RegistrationState state, String message) {
-                if (!SettingConfig.getSipEnabled(BaseApplication.appContext)) {
-                    return;
-                }
-
-                EventBus.getDefault().post(new MessageEvent(state, Constant.EVENT_SIP_REGISTER_STATUS));
-            }
-        };
-
-        try {
-            //复制一些源资源
-            //默认配置只能在首次时安装一次
-            copyIfNotExist(R.raw.linphonerc_default, basePath + "/.wdkl_sip_rc");
-            //用户配置,每次复制
-            copyFromPackage(R.raw.linphonerc_factory, "wdkl_sip_rc");
-        } catch (IOException ioe) {
-            Log.e("sipCall",ioe.getMessage());
-        }
-
-        //创建SIP核心并加载监听器
-        mCore = Factory.instance()
-                .createCore(basePath + "/.wdkl_sip_rc", basePath + "/wdkl_sip_rc", this);
-        mCore.addListener(mCoreListener);
-
-        //SIP核心配置完成
-        configureCore();
-    }
-
-    private Notification getNotification() {
-        Notification.Builder builder = new Notification.Builder(this)
-                .setSmallIcon(R.mipmap.ic_launcher)
-                .setContentTitle("sip service")
-                .setContentText("running...")
-                .setOnlyAlertOnce(true);
-
-        //设置Notification的ChannelID,否则不能正常显示
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            builder.setChannelId(notificationId);
-        }
-
-        return builder.build();
-    }
-
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            startForeground(11, getNotification()); //开前台服务
-        }
-
-        super.onStartCommand(intent, flags, startId);
-        //Toast.makeText(WdklSipService.this, "sip服务已启动", Toast.LENGTH_SHORT).show();
-
-        //如果服务已经在运行,则返回
-        if (sInstance != null) {
-            return START_STICKY;
-        }
-
-        //一旦服务启动,则一直保持
-        sInstance = this;
-
-        //SIP核心在创建和配置完成后,开启
-        mCore.start();
-        //必须定时运行 SIP核心 iterate()
-        TimerTask lTask = new TimerTask() {
-            @Override
-            public void run() {
-                mHandler.post(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mCore != null) {
-                                    mCore.iterate();
-                                }
-                            }
-                        });
-            }
-        };
-        mTimer = new Timer("wdkl sip scheduler");
-        mTimer.schedule(lTask, 0, 20);
-
-        return START_STICKY;
-    }
-
-    @Override
-    public void onDestroy() {
-        mCore.removeListener(mCoreListener);
-        mTimer.cancel();
-        mCore.stop();
-        // A stopped Core can be started again
-        // To ensure resources are freed, we must ensure it will be garbage collected
-        mCore = null;
-        // Don't forget to free the singleton as well
-        sInstance = null;
-
-        super.onDestroy();
-    }
-
-    @Override
-    public void onTaskRemoved(Intent rootIntent) {
-        // For this sample we will kill the Service at the same time we kill the app
-        stopSelf();
-
-        super.onTaskRemoved(rootIntent);
-    }
-
-    private void configureCore() {
-        // We will create a directory for user signed certificates if needed
-        String basePath = getFilesDir().getAbsolutePath();
-        String userCerts = basePath + "/user-certs";
-        File f = new File(userCerts);
-        if (!f.exists()) {
-            if (!f.mkdir()) {
-                Log.e("sipCall",userCerts + " can't be created.");
-            }
-        }
-        mCore.setUserCertificatesPath(userCerts);
-
-        //音频部分, 这里增加了一个遍历, 用于设置指定的音频格式.
-        PayloadType[] payloads = mCore.getAudioPayloadTypes();
-        for(int i = 0; i < payloads.length; i ++){
-            PayloadType pt = payloads[i];
-            //Log.i("sipCall", ">>>>>>>>>>>>>>>>>1 " + pt.getMimeType() + " = " + pt.enabled());
-            if (pt.getMimeType().equals("PCMU")
-//                    || pt.getMimeType().equals("PUMA")
-//                    || pt.getMimeType().equals("GSM")
-            ){
-                pt.enable(true);
-            } else {
-                pt.enable(false);
-            }
-        }
-        mCore.setAudioPayloadTypes(payloads);
-    }
-
-    private void dumpDeviceInformation() {
-        StringBuilder sb = new StringBuilder();
-        sb.append("DEVICE=").append(Build.DEVICE).append("\n");
-        sb.append("MODEL=").append(Build.MODEL).append("\n");
-        sb.append("MANUFACTURER=").append(Build.MANUFACTURER).append("\n");
-        sb.append("SDK=").append(Build.VERSION.SDK_INT).append("\n");
-        sb.append("Supported ABIs=");
-        for (String abi : Version.getCpuAbis()) {
-            sb.append(abi).append(", ");
-        }
-        sb.append("\n");
-        Log.i("sipCall",sb.toString());
-    }
-
-    private void dumpInstalledLinphoneInformation() {
-        PackageInfo info = null;
-        try {
-            info = getPackageManager().getPackageInfo(getPackageName(), 0);
-        } catch (PackageManager.NameNotFoundException nnfe) {
-            Log.e("sipCall",nnfe.getMessage());
-        }
-
-        if (info != null) {
-            Log.i(
-                    "[Service] sipphone version is ",
-                    info.versionName + " (" + info.versionCode + ")");
-        } else {
-            Log.i("sipCall","[Service] sipphone version is unknown");
-        }
-    }
-
-    private void copyIfNotExist(int ressourceId, String target) throws IOException {
-        File lFileToCopy = new File(target);
-        if (!lFileToCopy.exists()) {
-            copyFromPackage(ressourceId, lFileToCopy.getName());
-        }
-    }
-
-    private void copyFromPackage(int ressourceId, String target) throws IOException {
-        FileOutputStream lOutputStream = openFileOutput(target, 0);
-        InputStream lInputStream = getResources().openRawResource(ressourceId);
-        int readByte;
-        byte[] buff = new byte[8048];
-        while ((readByte = lInputStream.read(buff)) != -1) {
-            lOutputStream.write(buff, 0, readByte);
-        }
-        lOutputStream.flush();
-        lOutputStream.close();
-        lInputStream.close();
-    }
-}

+ 45 - 0
android_door/src/main/common/java/com/wdkl/ncs/host/sip/callback/PhoneCallback.java

@@ -0,0 +1,45 @@
+package com.wdkl.ncs.host.sip.callback;
+
+import org.linphone.core.Call;
+
+public abstract class PhoneCallback {
+    /**
+     * 来电状态
+     *
+     * @param call
+     */
+    public void incomingCall(Call call) {
+    }
+
+    /**
+     * 呼叫初始化
+     */
+    public void outgoingInit(Call call) {
+    }
+
+    /**
+     * 电话接通
+     */
+    public void callConnected(Call call) {
+    }
+
+    /**
+     * 电话挂断
+     */
+    public void callEnd(Call call) {
+    }
+
+    /**
+     * 释放通话
+     */
+    public void callReleased(Call call) {
+    }
+
+    /**
+     * 连接失败
+     */
+    public void error(String string) {
+    }
+
+
+}

+ 13 - 0
android_door/src/main/common/java/com/wdkl/ncs/host/sip/callback/RegistrationCallback.java

@@ -0,0 +1,13 @@
+package com.wdkl.ncs.host.sip.callback;
+
+public abstract class RegistrationCallback {
+    public void registrationNone() {}
+
+    public void registrationProgress() {}
+
+    public void registrationOk() {}
+
+    public void registrationCleared() {}
+
+    public void registrationFailed() {}
+}

+ 540 - 0
android_door/src/main/common/java/com/wdkl/ncs/host/sip/core/CorePreferences.kt

@@ -0,0 +1,540 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.wdkl.ncs.host.sip.core
+
+import android.content.Context
+import android.util.Log
+import com.wdkl.app.ncs.callingdoor.R
+import org.linphone.core.BuildConfig
+import org.linphone.core.Config
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+class CorePreferences constructor(private val context: Context) {
+    private var _config: Config? = null
+    var config: Config
+        get() = _config!!
+        set(value) {
+            _config = value
+        }
+
+    /* VFS encryption */
+
+    companion object {
+        private const val encryptedSharedPreferencesFile = "encrypted.pref"
+    }
+
+
+
+    /* App settings */
+
+    var debugLogs: Boolean
+        get() = config.getBool("app", "debug", BuildConfig.DEBUG)
+        set(value) {
+            config.setBool("app", "debug", value)
+        }
+
+    var autoStart: Boolean
+        get() = config.getBool("app", "auto_start", true)
+        set(value) {
+            config.setBool("app", "auto_start", value)
+        }
+
+    var keepServiceAlive: Boolean
+        get() = config.getBool("app", "keep_service_alive", false)
+        set(value) {
+            config.setBool("app", "keep_service_alive", value)
+        }
+
+    var readAndAgreeTermsAndPrivacy: Boolean
+        get() = config.getBool("app", "read_and_agree_terms_and_privacy", false)
+        set(value) {
+            config.setBool("app", "read_and_agree_terms_and_privacy", value)
+        }
+
+    /* UI */
+
+    var forcePortrait: Boolean
+        get() = config.getBool("app", "force_portrait_orientation", false)
+        set(value) {
+            config.setBool("app", "force_portrait_orientation", value)
+        }
+
+    var replaceSipUriByUsername: Boolean
+        get() = config.getBool("app", "replace_sip_uri_by_username", false)
+        set(value) {
+            config.setBool("app", "replace_sip_uri_by_username", value)
+        }
+
+    var enableAnimations: Boolean
+        get() = config.getBool("app", "enable_animations", false)
+        set(value) {
+            config.setBool("app", "enable_animations", value)
+        }
+
+    /** -1 means auto, 0 no, 1 yes */
+    var darkMode: Int
+        get() {
+            if (!darkModeAllowed) return 0
+            return config.getInt("app", "dark_mode", -1)
+        }
+        set(value) {
+            config.setInt("app", "dark_mode", value)
+        }
+
+    /* Audio */
+
+    /* Video */
+
+    var videoPreview: Boolean
+        get() = config.getBool("app", "video_preview", false)
+        set(value) = config.setBool("app", "video_preview", value)
+
+    /* Chat */
+
+    // iOS and Android 4.4.x releases currently can't display more than 1 file per message
+    // TODO: Remove for the release, this won't be necessary anymore
+    var preventMoreThanOneFilePerMessage: Boolean
+        get() = config.getBool("app", "prevent_more_than_one_file_per_message", true)
+        set(value) {
+            config.setBool("app", "prevent_more_than_one_file_per_message", value)
+        }
+
+    var markAsReadUponChatMessageNotificationDismissal: Boolean
+        get() = config.getBool("app", "mark_as_read_notif_dismissal", false)
+        set(value) {
+            config.setBool("app", "mark_as_read_notif_dismissal", value)
+        }
+
+    var makePublicMediaFilesDownloaded: Boolean
+        // Keep old name for backward compatibility
+        get() = config.getBool("app", "make_downloaded_images_public_in_gallery", true)
+        set(value) {
+            config.setBool("app", "make_downloaded_images_public_in_gallery", value)
+        }
+
+    var useInAppFileViewerForNonEncryptedFiles: Boolean
+        get() = config.getBool("app", "use_in_app_file_viewer_for_non_encrypted_files", false)
+        set(value) {
+            config.setBool("app", "use_in_app_file_viewer_for_non_encrypted_files", value)
+        }
+
+    var hideChatMessageContentInNotification: Boolean
+        get() = config.getBool("app", "hide_chat_message_content_in_notification", false)
+        set(value) {
+            config.setBool("app", "hide_chat_message_content_in_notification", value)
+        }
+
+    var hideEmptyRooms: Boolean
+        get() = config.getBool("app", "hide_empty_chat_rooms", true)
+        set(value) {
+            config.setBool("app", "hide_empty_chat_rooms", value)
+        }
+
+    var hideRoomsFromRemovedProxies: Boolean
+        get() = config.getBool("app", "hide_chat_rooms_from_removed_proxies", true)
+        set(value) {
+            config.setBool("app", "hide_chat_rooms_from_removed_proxies", value)
+        }
+
+    var deviceName: String
+        get() = config.getString("app", "device_name", "Android")!!
+        set(value) = config.setString("app", "device_name", value)
+
+    var chatRoomShortcuts: Boolean
+        get() = config.getBool("app", "chat_room_shortcuts", true)
+        set(value) {
+            config.setBool("app", "chat_room_shortcuts", value)
+        }
+
+    /* Contacts */
+
+    var storePresenceInNativeContact: Boolean
+        get() = config.getBool("app", "store_presence_in_native_contact", false)
+        set(value) {
+            config.setBool("app", "store_presence_in_native_contact", value)
+        }
+
+    var showNewContactAccountDialog: Boolean
+        get() = config.getBool("app", "show_new_contact_account_dialog", true)
+        set(value) {
+            config.setBool("app", "show_new_contact_account_dialog", value)
+        }
+
+    var displayOrganization: Boolean
+        get() = config.getBool("app", "display_contact_organization", contactOrganizationVisible)
+        set(value) {
+            config.setBool("app", "display_contact_organization", value)
+        }
+
+    var contactsShortcuts: Boolean
+        get() = config.getBool("app", "contact_shortcuts", false)
+        set(value) {
+            config.setBool("app", "contact_shortcuts", value)
+        }
+
+    /* Call */
+
+    var acceptEarlyMedia: Boolean
+        get() = config.getBool("sip", "incoming_calls_early_media", false)
+        set(value) {
+            config.setBool("sip", "incoming_calls_early_media", value)
+        }
+
+    var autoAnswerEnabled: Boolean
+        get() = config.getBool("app", "auto_answer", false)
+        set(value) {
+            config.setBool("app", "auto_answer", value)
+        }
+
+    var autoAnswerDelay: Int
+        get() = config.getInt("app", "auto_answer_delay", 0)
+        set(value) {
+            config.setInt("app", "auto_answer_delay", value)
+        }
+
+    // Show overlay inside of application
+    var showCallOverlay: Boolean
+        get() = config.getBool("app", "call_overlay", true)
+        set(value) {
+            config.setBool("app", "call_overlay", value)
+        }
+
+    // Show overlay even when app is in background, requires permission
+    var systemWideCallOverlay: Boolean
+        get() = config.getBool("app", "system_wide_call_overlay", false)
+        set(value) {
+            config.setBool("app", "system_wide_call_overlay", value)
+        }
+
+    var callRightAway: Boolean
+        get() = config.getBool("app", "call_right_away", false)
+        set(value) {
+            config.setBool("app", "call_right_away", value)
+        }
+
+    var fullScreenCallUI: Boolean
+        get() = config.getBool("app", "full_screen_call", true)
+        set(value) {
+            config.setBool("app", "full_screen_call", value)
+        }
+
+    var routeAudioToBluetoothIfAvailable: Boolean
+        get() = config.getBool("app", "route_audio_to_bluetooth_if_available", false)
+        set(value) {
+            config.setBool("app", "route_audio_to_bluetooth_if_available", value)
+        }
+
+    // This won't be done if bluetooth or wired headset is used
+    var routeAudioToSpeakerWhenVideoIsEnabled: Boolean
+        get() = config.getBool("app", "route_audio_to_speaker_when_video_enabled", false)
+        set(value) {
+            config.setBool("app", "route_audio_to_speaker_when_video_enabled", value)
+        }
+
+    /* Assistant */
+
+    var firstStart: Boolean
+        get() = config.getBool("app", "first_start", true)
+        set(value) {
+            config.setBool("app", "first_start", value)
+        }
+
+    var xmlRpcServerUrl: String?
+        get() = config.getString("assistant", "xmlrpc_url", null)
+        set(value) {
+            config.setString("assistant", "xmlrpc_url", value)
+        }
+
+    /* Dialog related */
+
+    var limeSecurityPopupEnabled: Boolean
+        get() = config.getBool("app", "lime_security_popup_enabled", true)
+        set(value) {
+            config.setBool("app", "lime_security_popup_enabled", value)
+        }
+
+    /* Other */
+
+    var voiceMailUri: String?
+        get() = config.getString("app", "voice_mail", null)
+        set(value) {
+            config.setString("app", "voice_mail", value)
+        }
+
+    var redirectDeclinedCallToVoiceMail: Boolean
+        get() = config.getBool("app", "redirect_declined_call_to_voice_mail", true)
+        set(value) {
+            config.setBool("app", "redirect_declined_call_to_voice_mail", value)
+        }
+
+    var lastUpdateAvailableCheckTimestamp: Int
+        get() = config.getInt("app", "version_check_url_last_timestamp", 0)
+        set(value) {
+            config.setInt("app", "version_check_url_last_timestamp", value)
+        }
+
+    var defaultAccountAvatarPath: String?
+        get() = config.getString("app", "default_avatar_path", null)
+        set(value) {
+            config.setString("app", "default_avatar_path", value)
+        }
+
+    /* *** Read only application settings, some were previously in non_localizable_custom *** */
+
+    /* UI related */
+
+    val hideContactsWithoutPresence: Boolean
+        get() = config.getBool("app", "hide_contacts_without_presence", false)
+
+    val contactOrganizationVisible: Boolean
+        get() = config.getBool("app", "display_contact_organization", true)
+
+    val showBorderOnContactAvatar: Boolean
+        get() = config.getBool("app", "show_border_on_contact_avatar", false)
+
+    val showBorderOnBigContactAvatar: Boolean
+        get() = config.getBool("app", "show_border_on_big_contact_avatar", true)
+
+    private val darkModeAllowed: Boolean
+        get() = config.getBool("app", "dark_mode_allowed", true)
+
+    /* Feature related */
+
+    val showScreenshotButton: Boolean
+        get() = config.getBool("app", "show_take_screenshot_button_in_call", false)
+
+    val dtmfKeypadVibration: Boolean
+        get() = config.getBool("app", "dtmf_keypad_vibraton", false)
+
+    val allowMultipleFilesAndTextInSameMessage: Boolean
+        get() = config.getBool("app", "allow_multiple_files_and_text_in_same_message", true)
+
+    val fetchContactsFromDefaultDirectory: Boolean
+        get() = config.getBool("app", "fetch_contacts_from_default_directory", true)
+
+    val hideStaticImageCamera: Boolean
+        get() = config.getBool("app", "hide_static_image_camera", true)
+
+    // Will disable chat feature completely
+    val disableChat: Boolean
+        get() = config.getBool("app", "disable_chat_feature", false)
+
+    // If enabled, this will cause the video to "freeze" on your correspondent screen
+    // as you won't send video packets anymore
+    val hideCameraPreviewInPipMode: Boolean
+        get() = config.getBool("app", "hide_camera_preview_in_pip_mode", false)
+
+    // This will prevent UI from showing up, except for the launcher & the foreground service notification
+    val preventInterfaceFromShowingUp: Boolean
+        get() = config.getBool("app", "keep_app_invisible", false)
+
+    /* Default values related */
+
+    val echoCancellerCalibration: Int
+        get() = config.getInt("sound", "ec_delay", -1)
+
+    val defaultDomain: String
+        get() = config.getString("app", "default_domain", "sip.linphone.org")!!
+
+    val debugPopupCode: String
+        get() = config.getString("app", "debug_popup_magic", "#1234#")!!
+
+    val conferenceServerUri: String
+        get() = config.getString(
+            "app",
+            "default_conference_factory_uri",
+            "sip:conference-factory@sip.linphone.org"
+        )!!
+
+    val limeX3dhServerUrl: String
+        get() = config.getString(
+            "app",
+            "default_lime_x3dh_server_url",
+            "https://lime.linphone.org/lime-server/lime-server.php"
+        )!!
+
+    val checkIfUpdateAvailableUrl: String?
+        get() = config.getString(
+            "misc",
+            "version_check_url_root",
+            "https://linphone.org/releases/android/RELEASE"
+        )
+
+    val checkUpdateAvailableInterval: Int
+        get() = config.getInt("app", "version_check_interval", 86400000)
+
+    /* Assistant */
+
+    val showCreateAccount: Boolean
+        get() = config.getBool("app", "assistant_create_account", true)
+
+    val showLinphoneLogin: Boolean
+        get() = config.getBool("app", "assistant_linphone_login", true)
+
+    val showGenericLogin: Boolean
+        get() = config.getBool("app", "assistant_generic_login", true)
+
+    val showRemoteProvisioning: Boolean
+        get() = config.getBool("app", "assistant_remote_provisioning", true)
+
+    /* Side Menu */
+
+    val showAccountsInSideMenu: Boolean
+        get() = config.getBool("app", "side_menu_accounts", true)
+
+    val showAssistantInSideMenu: Boolean
+        get() = config.getBool("app", "side_menu_assistant", true)
+
+    val showSettingsInSideMenu: Boolean
+        get() = config.getBool("app", "side_menu_settings", true)
+
+    val showRecordingsInSideMenu: Boolean
+        get() = config.getBool("app", "side_menu_recordings", true)
+
+    val showAboutInSideMenu: Boolean
+        get() = config.getBool("app", "side_menu_about", true)
+
+    val showQuitInSideMenu: Boolean
+        get() = config.getBool("app", "side_menu_quit", true)
+
+    /* Settings */
+
+    val allowDtlsTransport: Boolean
+        get() = config.getBool("app", "allow_dtls_transport", false)
+
+    val showAccountSettings: Boolean
+        get() = config.getBool("app", "settings_accounts", true)
+
+    val showTunnelSettings: Boolean
+        get() = config.getBool("app", "settings_tunnel", true)
+
+    val showAudioSettings: Boolean
+        get() = config.getBool("app", "settings_audio", true)
+
+    val showVideoSettings: Boolean
+        get() = config.getBool("app", "settings_video", true)
+
+    val showCallSettings: Boolean
+        get() = config.getBool("app", "settings_call", true)
+
+    val showChatSettings: Boolean
+        get() = config.getBool("app", "settings_chat", true)
+
+    val showNetworkSettings: Boolean
+        get() = config.getBool("app", "settings_network", true)
+
+    val showContactsSettings: Boolean
+        get() = config.getBool("app", "settings_contacts", true)
+
+    val showAdvancedSettings: Boolean
+        get() = config.getBool("app", "settings_advanced", true)
+
+    /* Assets stuff */
+
+    val configPath: String
+        get() = context.filesDir.absolutePath + "/.linphonerc"
+
+    val factoryConfigPath: String
+        get() = context.filesDir.absolutePath + "/linphonerc"
+
+    val linphoneDefaultValuesPath: String
+        get() = context.filesDir.absolutePath + "/assistant_linphone_default_values"
+
+    val defaultValuesPath: String
+        get() = context.filesDir.absolutePath + "/assistant_default_values"
+
+    val ringtonePath: String
+        get() = context.filesDir.absolutePath + "/share/sounds/linphone/rings/notes_of_the_optimistic.mkv"
+
+    val userCertificatesPath: String
+        get() = context.filesDir.absolutePath + "/user-certs"
+
+    val staticPicturePath: String
+        get() = context.filesDir.absolutePath + "/share/images/nowebcamcif.jpg"
+
+    fun copyAssetsFromPackage() {
+        copy("linphonerc_default", configPath)
+        copy("linphonerc_factory", factoryConfigPath, true)
+        copy("assistant_linphone_default_values", linphoneDefaultValuesPath, true)
+        copy("assistant_default_values", defaultValuesPath, true)
+
+        move(context.filesDir.absolutePath + "/linphone-log-history.db", context.filesDir.absolutePath + "/call-history.db")
+        move(context.filesDir.absolutePath + "/zrtp_secrets", context.filesDir.absolutePath + "/zrtp-secrets.db")
+    }
+
+    fun getString(resource: Int): String {
+        return context.getString(resource)
+    }
+
+    private fun copy(from: String, to: String, overrideIfExists: Boolean = false) {
+        val outFile = File(to)
+        if (outFile.exists()) {
+            if (!overrideIfExists) {
+                Log.i(context.getString(R.string.app_name), "[Preferences] File $to already exists")
+                return
+            }
+        }
+        Log.i(context.getString(R.string.app_name), "[Preferences] Overriding $to by $from asset")
+
+        val outStream = FileOutputStream(outFile)
+        val inFile = context.assets.open(from)
+        val buffer = ByteArray(1024)
+        var length: Int = inFile.read(buffer)
+
+        while (length > 0) {
+            outStream.write(buffer, 0, length)
+            length = inFile.read(buffer)
+        }
+
+        inFile.close()
+        outStream.flush()
+        outStream.close()
+    }
+
+    private fun move(from: String, to: String, overrideIfExists: Boolean = false) {
+        val inFile = File(from)
+        val outFile = File(to)
+        if (inFile.exists()) {
+            if (outFile.exists() && !overrideIfExists) {
+                Log.w(context.getString(R.string.app_name), "[Preferences] Can't move [$from] to [$to], destination file already exists")
+            } else {
+                val inStream = FileInputStream(inFile)
+                val outStream = FileOutputStream(outFile)
+
+                val buffer = ByteArray(1024)
+                var read: Int
+                while (inStream.read(buffer).also { read = it } != -1) {
+                    outStream.write(buffer, 0, read)
+                }
+
+                inStream.close()
+                outStream.flush()
+                outStream.close()
+
+                inFile.delete()
+                Log.i(context.getString(R.string.app_name), "[Preferences] Successfully moved [$from] to [$to]")
+            }
+        } else {
+            Log.w(context.getString(R.string.app_name), "[Preferences] Can't move [$from] to [$to], source file doesn't exists")
+        }
+    }
+}

+ 248 - 0
android_door/src/main/common/java/com/wdkl/ncs/host/sip/core/LinCoreService.java

@@ -0,0 +1,248 @@
+/*
+ * Copyright (c) 2010-2022 Belledonne Communications SARL.
+ *
+ * This file is part of Liblinphone
+ * (see https://gitlab.linphone.org/BC/public/liblinphone).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.wdkl.ncs.host.sip.core;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Vibrator;
+
+import org.linphone.core.Call;
+import org.linphone.core.Core;
+import org.linphone.core.CoreListenerStub;
+import org.linphone.core.Factory;
+import org.linphone.core.tools.Log;
+import org.linphone.core.tools.compatibility.DeviceUtils;
+
+/**
+ * This service is used to monitor activities lifecycle and detect when app is in background/foreground.
+ * It is also used as a foreground service while at least one call is running to prevent the app from getting killed.
+ * Finally when task is removed, it will stop itself and the Core.
+ */
+public class LinCoreService extends Service {
+    protected static final int SERVICE_NOTIF_ID = 1;
+    protected static final String SERVICE_NOTIFICATION_CHANNEL_ID = "sip_core_service_notification_channel";
+    protected static final String SERVICE_NOTIFICATION_CHANNEL_NAME = "Sip Core Service";
+    protected static final String SERVICE_NOTIFICATION_CHANNEL_DESC = "keep the call(s) alive";
+    protected static final String SERVICE_NOTIFICATION_TITLE = "Sip Core Service";
+    protected static final String SERVICE_NOTIFICATION_CONTENT = "keep the call(s) alive";
+
+    protected boolean mIsInForegroundMode = false;
+    protected Notification mServiceNotification = null;
+
+    private CoreListenerStub mListener;
+    private Vibrator mVibrator;
+    private boolean mIsVibrating;
+    private AudioManager mAudioManager;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        // No-op, just to ensure libraries have been loaded and thus prevent crash in log below
+        // if service has been started directly by Android (that can happen...)
+        Factory.instance();
+
+        createServiceNotificationChannel();
+
+        mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+        mListener = new CoreListenerStub() {
+            public void onFirstCallStarted(Core core) {
+                Log.i("[Core Service] First call started");
+                if (!mIsInForegroundMode) {
+                    startForeground();
+                }
+
+                /*Call call = core.getCurrentCall();
+                if (call != null) {
+                    if (call.getDir() == Call.Dir.Incoming && core.isVibrationOnIncomingCallEnabled()) {
+                        vibrate();
+                    }
+                } else {
+                    Log.w("[Core Service] Couldn't find current call...");
+                }*/
+
+            }
+
+            public void onCallStateChanged(Core core, Call call, Call.State state, String message) {
+                /*if ((state == Call.State.End || state == Call.State.Error || state == Call.State.Connected) && mIsVibrating) {
+                    Log.i("[Core Service] Stopping vibrator");
+                    mVibrator.cancel();
+                    mIsVibrating = false;
+                }*/
+
+            }
+
+            public void onLastCallEnded(Core core) {
+                Log.i("[Core Service] Last call ended");
+                if (mIsInForegroundMode) {
+                    stopForeground();
+                }
+
+            }
+        };
+
+        /*if (CoreManager.isReady()) {
+            Core core = CoreManager.instance().getCore();
+            if (core != null) {
+                Log.i("[Core Service] Core Manager found, adding our listener");
+                core.addListener(mListener);
+                if (core.getCallsNb() > 0) {
+                    Log.w("[Core Service] Service started while at least one call active !");
+                    startForeground();
+                    Call call = core.getCurrentCall();
+                    if (call != null) {
+                        if (call.getDir() == Call.Dir.Incoming && call.getState() == Call.State.IncomingReceived && core.isVibrationOnIncomingCallEnabled()) {
+                            vibrate();
+                        }
+                    } else {
+                        Log.w("[Core Service] Couldn't find current call...");
+                    }
+                }
+            }
+        }*/
+
+        Log.i("[Core Service] Created");
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        super.onStartCommand(intent, flags, startId);
+        Log.i("[Core Service] Started");
+
+        return START_STICKY;
+    }
+
+    @Override
+    public void onTaskRemoved(Intent rootIntent) {
+        Log.i("[Core Service] Task removed");
+        super.onTaskRemoved(rootIntent);
+    }
+
+    @Override
+    public synchronized void onDestroy() {
+        Log.i("[Core Service] Stopping");
+        /*if (CoreManager.isReady()) {
+            Core core = CoreManager.instance().getCore();
+            if (core != null) {
+                Log.i("[Core Service] Core Manager found, removing our listener");
+                core.removeListener(mListener);
+            }
+        }*/
+
+        super.onDestroy();
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    /**
+     * This method should create a notification channel for the foreground service notification.
+     * On Android < 8 it is not called.
+     */
+    public void createServiceNotificationChannel() {
+        Log.i("[Core Service] Android >= 8.0 detected, creating notification channel");
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+            NotificationChannel channel = new NotificationChannel(SERVICE_NOTIFICATION_CHANNEL_ID, SERVICE_NOTIFICATION_CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW);
+            channel.setDescription(SERVICE_NOTIFICATION_CHANNEL_DESC);
+            channel.enableVibration(false);
+            channel.enableLights(false);
+            channel.setShowBadge(false);
+            channel.setBypassDnd(true);
+            channel.setSound(null, null);
+            notificationManager.createNotificationChannel(channel);
+        }
+    }
+
+    public void createServiceNotification() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mServiceNotification = new Notification.Builder(this, SERVICE_NOTIFICATION_CHANNEL_ID)
+                    .setContentTitle(SERVICE_NOTIFICATION_TITLE)
+                    .setContentText(SERVICE_NOTIFICATION_CONTENT)
+                    .setSmallIcon(getApplicationInfo().icon)
+                    .setAutoCancel(false)
+                    .setCategory(Notification.CATEGORY_SERVICE)
+                    .setVisibility(Notification.VISIBILITY_SECRET)
+                    .setWhen(System.currentTimeMillis())
+                    .setShowWhen(true)
+                    .setOngoing(true)
+                    .build();
+        }
+    }
+
+    public void showForegroundServiceNotification() {
+        if (mServiceNotification == null) {
+            createServiceNotification();
+        }
+
+        startForeground(SERVICE_NOTIF_ID, mServiceNotification);
+    }
+
+    public void hideForegroundServiceNotification() {
+        stopForeground(true);
+    }
+
+    void startForeground() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            Log.i("[Core Service] Starting service as foreground");
+            showForegroundServiceNotification();
+            mIsInForegroundMode = true;
+        }
+    }
+
+    void stopForeground() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            if (!mIsInForegroundMode) {
+                Log.w("[Core Service] Service isn't in foreground mode, nothing to do");
+            } else {
+                Log.i("[Core Service] Stopping service as foreground");
+                hideForegroundServiceNotification();
+                mIsInForegroundMode = false;
+            }
+        }
+    }
+
+    private void vibrate() {
+        if (mVibrator != null && mVibrator.hasVibrator()) {
+            if (mAudioManager.getRingerMode() == 0) {
+                Log.i("[Core Service] Do not vibrate as ringer mode is set to silent");
+            } else {
+                Log.i("[Core Service] Starting vibrator");
+                DeviceUtils.vibrate(mVibrator);
+                mIsVibrating = true;
+            }
+        } else {
+            Log.e("[Core Service] Device doesn't have a vibrator");
+        }
+
+    }
+}

+ 521 - 0
android_door/src/main/common/java/com/wdkl/ncs/host/sip/core/LinphoneManager.kt

@@ -0,0 +1,521 @@
+package com.wdkl.ncs.host.sip.core
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Build
+import android.telephony.PhoneStateListener
+import android.telephony.TelephonyManager
+import android.util.Log
+import android.view.TextureView
+import com.wdkl.ncs.host.sip.callback.PhoneCallback
+import com.wdkl.ncs.host.sip.callback.RegistrationCallback
+import com.wdkl.ncs.host.sip.utils.AudioRouteUtils
+import com.wdkl.ncs.host.sip.utils.LinphoneUtils
+import com.wdkl.ncs.host.sip.utils.VideoZoomHelper
+import org.linphone.core.*
+import java.io.File
+import java.util.*
+
+
+class LinphoneManager private constructor(private val context: Context) {
+    private val TAG = "sip_linphone"
+
+    private var core: Core
+    private var corePreferences: CorePreferences
+    private var coreIsStart = false
+    var registrationCallback: RegistrationCallback? = null
+    var phoneCallback: PhoneCallback? = null
+
+
+    init {
+        //日志收集
+        Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
+        Factory.instance().enableLogCollection(LogCollectionState.Disabled)
+
+        corePreferences = CorePreferences(context)
+        corePreferences.copyAssetsFromPackage()
+        val config = Factory.instance().createConfigWithFactory(
+            corePreferences.configPath,
+            corePreferences.factoryConfigPath
+        )
+        corePreferences.config = config
+
+        Factory.instance().setDebugMode(false, "SipManager")
+
+        core = Factory.instance().createCoreWithConfig(config, context)
+        core.reloadSoundDevices()
+    }
+
+    private var previousCallState = Call.State.Idle
+
+    private val coreListener = object : CoreListenerStub() {
+        /*override fun onGlobalStateChanged(core: Core, state: GlobalState?, message: String) {
+            if (state === GlobalState.On) {
+            }
+        }*/
+
+        //登录状态回调
+        /*override fun onRegistrationStateChanged(
+            core: Core,
+            cfg: ProxyConfig,
+            state: RegistrationState,
+            message: String
+        ) {
+            when (state) {
+                RegistrationState.None -> registrationCallback?.registrationNone()
+                RegistrationState.Progress -> registrationCallback?.registrationProgress()
+                RegistrationState.Ok -> registrationCallback?.registrationOk()
+                RegistrationState.Cleared -> registrationCallback?.registrationCleared()
+                RegistrationState.Failed -> registrationCallback?.registrationFailed()
+            }
+        }*/
+
+        //电话状态回调
+        override fun onCallStateChanged(
+            core: Core,
+            call: Call,
+            state: Call.State,
+            message: String
+        ) {
+            Log.i(TAG, "[Context] Call state changed [$state]")
+
+            when (state) {
+                Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> {
+                    if (gsmCallActive) {
+                        Log.w(TAG, "[Context] Refusing the call with reason busy because a GSM call is active")
+                        call.decline(Reason.Busy)
+                        return
+                    }
+
+                    phoneCallback?.incomingCall(call)
+                    gsmCallActive = true
+
+                    //自动接听
+                    /*if (corePreferences.autoAnswerEnabled) {
+                        val autoAnswerDelay = corePreferences.autoAnswerDelay
+                        if (autoAnswerDelay == 0) {
+                            Log.w(TAG, "[Context] Auto answering call immediately")
+                            answerCall(call)
+                        } else {
+                            Log.i(
+                                TAG,
+                                "[Context] Scheduling auto answering in $autoAnswerDelay milliseconds"
+                            )
+                            val mainThreadHandler = Handler(Looper.getMainLooper())
+                            mainThreadHandler.postDelayed({
+                                Log.w(TAG, "[Context] Auto answering call")
+                                answerCall(call) },
+                                autoAnswerDelay.toLong())
+                        }
+                    }*/
+                }
+
+                Call.State.OutgoingInit -> {
+                    phoneCallback?.outgoingInit(call)
+                    gsmCallActive = true
+                }
+
+                /*Call.State.OutgoingProgress -> {
+                    if (core.callsNb == 1 && corePreferences.routeAudioToBluetoothIfAvailable) {
+                        AudioRouteUtils.routeAudioToBluetooth(core, call)
+                    }
+                }*/
+
+                Call.State.Connected -> {
+                    phoneCallback?.callConnected(call)
+                }
+
+                Call.State.StreamsRunning -> {
+                    Log.i(TAG, "StreamsRunning...")
+                    // Do not automatically route audio to bluetooth after first call
+                    /*if (core.callsNb == 1) {
+                        // Only try to route bluetooth / headphone / headset when the call is in StreamsRunning for the first time
+                        if (previousCallState == Call.State.Connected) {
+                            Log.i(TAG, "[Context] First call going into StreamsRunning state for the first time, trying to route audio to headset or bluetooth if available")
+                            if (AudioRouteUtils.isHeadsetAudioRouteAvailable(core)) {
+                                AudioRouteUtils.routeAudioToHeadset(core, call)
+                            } else if (corePreferences.routeAudioToBluetoothIfAvailable && AudioRouteUtils.isBluetoothAudioRouteAvailable(core)) {
+                                AudioRouteUtils.routeAudioToBluetooth(core, call)
+                            }
+                        }
+                    }
+
+                    if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.videoEnabled()) {
+                        // Do not turn speaker on when video is enabled if headset or bluetooth is used
+                        if (!AudioRouteUtils.isHeadsetAudioRouteAvailable(core) &&
+                            !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(core, call)
+                        ) {
+                            Log.i(TAG, "[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker")
+                            AudioRouteUtils.routeAudioToSpeaker(core, call)
+                        }
+                    }*/
+                }
+
+                Call.State.End, Call.State.Released, Call.State.Error -> {
+                    //if (core.callsNb == 0) {
+                        when (state) {
+                            Call.State.End -> phoneCallback?.callEnd(call)
+
+                            Call.State.Released -> phoneCallback?.callReleased(call)
+
+                            Call.State.Error -> {
+                                phoneCallback?.error(call.errorInfo.reason.toString())
+                            }
+                        }
+                        gsmCallActive = false
+                    //}
+                }
+            }
+            previousCallState = state
+        }
+    }
+
+    /**
+     * 启动linphone
+     */
+    fun start() {
+        if (!coreIsStart) {
+            coreIsStart = true
+            Log.i(TAG, "[Context] Starting")
+            core.addListener(coreListener)
+            core.start()
+
+            initLinphone()
+
+            val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+            Log.i(TAG, "[Context] Registering phone state listener")
+            telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE)
+        }
+    }
+
+    /**
+     * 停止linphone
+     */
+    fun stop() {
+        coreIsStart = false
+        val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+
+        Log.i(TAG, "[Context] Unregistering phone state listener")
+        telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
+
+        core.removeListener(coreListener)
+        core.stop()
+    }
+
+
+    /**
+     * 注册到服务器
+     *
+     * @param username     账号名
+     * @param password      密码
+     * @param domain     IP地址:端口号
+     */
+    fun createProxyConfig(
+        username: String,
+        password: String,
+        domain: String,
+        type: TransportType? = TransportType.Udp
+    ) {
+        core.clearProxyConfig()
+
+        val accountCreator = core.createAccountCreator(corePreferences.xmlRpcServerUrl)
+        accountCreator.language = Locale.getDefault().language
+        accountCreator.reset()
+
+        accountCreator.username = username
+        accountCreator.password = password
+        accountCreator.domain = domain
+        accountCreator.displayName = username
+        accountCreator.transport = type
+
+        accountCreator.createProxyConfig()
+    }
+
+
+    /**
+     * 取消注册
+     */
+    fun removeInvalidProxyConfig() {
+        core.clearProxyConfig()
+
+    }
+
+    fun getCore(): Core {
+        return core
+    }
+
+    /**
+     * 拨打电话
+     * @param to String
+     * @param isVideoCall Boolean
+     */
+    fun startCall(to: String, isVideoCall: Boolean) {
+        try {
+            val addressToCall = core.interpretUrl(to)
+            addressToCall?.displayName = to
+            val params = core.createCallParams(null)
+            //启用通话录音
+            //params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(context, addressToCall!!)
+            //启动低宽带模式
+            if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
+                Log.w(TAG, "[Context] Enabling low bandwidth mode!")
+                params?.enableLowBandwidth(true)
+            }
+            if (isVideoCall) {
+                params?.enableVideo(true)
+                core.enableVideoCapture(true)
+                core.enableVideoDisplay(true)
+            } else {
+                params?.enableVideo(false)
+            }
+            if (params != null) {
+                core.inviteAddressWithParams(addressToCall!!, params)
+            } else {
+                core.inviteAddress(addressToCall!!)
+            }
+            Log.d(TAG, ">>>>>>>>>>> invite address: " + addressToCall.asString())
+
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+
+    }
+
+
+    /**
+     * 接听来电
+     *
+     */
+    fun answerCall(call: Call, supportVideo: Boolean) {
+        Log.i(TAG, "[Context] Answering call $call")
+        val params = core.createCallParams(call)
+        //启用通话录音
+        //params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(context, call.remoteAddress)
+        if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
+            Log.w(TAG, "[Context] Enabling low bandwidth mode!")
+            params?.enableLowBandwidth(true)
+        }
+        if (supportVideo) {
+            params?.enableVideo(isVideoCall(call))
+        }
+        call.acceptWithParams(params)
+    }
+
+    /**
+     * 谢绝电话
+     * @param call Call
+     */
+    fun declineCall(call: Call) {
+        val voiceMailUri = corePreferences.voiceMailUri
+        if (voiceMailUri != null && corePreferences.redirectDeclinedCallToVoiceMail) {
+            val voiceMailAddress = core.interpretUrl(voiceMailUri)
+            if (voiceMailAddress != null) {
+                Log.i(TAG, "[Context] Redirecting call $call to voice mail URI: $voiceMailUri")
+                call.redirectTo(voiceMailAddress)
+            }
+        } else {
+            Log.i(TAG, "[Context] Declining call $call")
+            call.decline(Reason.Declined)
+        }
+    }
+
+    /**
+     * 挂断电话
+     */
+    fun terminateCall(call: Call) {
+        Log.i(TAG, "[Context] Terminating call $call")
+        call.terminate()
+    }
+
+    fun terminateCall() {
+        Log.i(TAG, "[Context] Terminating call: " + core.callsNb)
+        if (core.callsNb > 0) {
+            var call = core.currentCall
+            if (call == null) {
+                call = core.calls[0]
+            }
+            call?.terminate()
+        }
+    }
+
+    fun micEnabled() = core.micEnabled()
+
+    fun speakerEnabled() = core.outputAudioDevice?.type == AudioDevice.Type.Speaker
+
+    /**
+     * 启动麦克风
+     * @param micEnabled Boolean
+     */
+    fun enableMic(micEnabled: Boolean) {
+        core.enableMic(micEnabled)
+    }
+
+    /**
+     * 扬声器或听筒
+     * @param speakerEnabled Boolean
+     */
+    fun enableSpeaker(speakerEnabled: Boolean) {
+        //core.reloadSoundDevices()
+        Log.e(TAG, "audio enable speaker: $speakerEnabled")
+        if (speakerEnabled) {
+            AudioRouteUtils.routeAudioToSpeaker(core)
+        } else {
+            AudioRouteUtils.routeAudioToEarpiece(core)
+        }
+
+        //Log.e(TAG, "input device: " + core.defaultInputAudioDevice.type + ", output device: " + core.defaultOutputAudioDevice.type)
+    }
+
+    fun reloadSoundDevice() {
+        core.reloadSoundDevices()
+    }
+
+    /**
+     * 是否是视频电话
+     * @return Boolean
+     */
+    fun isVideoCall(call: Call): Boolean {
+        val remoteParams = call.remoteParams
+        return remoteParams != null && remoteParams.videoEnabled()
+    }
+
+
+    /**
+     * 设置视频界面
+     * @param videoRendering TextureView 对方界面
+     * @param videoPreview CaptureTextureView 自己界面
+     */
+    fun setVideoWindowId(videoRendering: TextureView?, videoPreview: TextureView?) {
+        if (videoRendering != null) {
+            core.nativeVideoWindowId = videoRendering
+        }
+        if (videoPreview != null) {
+            core.enableVideoPreview(true)
+            core.nativePreviewWindowId = videoPreview
+        }
+    }
+
+    /**
+     * 设置视频电话可缩放
+     * @param context Context
+     * @param videoRendering TextureView
+     */
+    fun setVideoZoom(context: Context, videoRendering: TextureView) {
+        VideoZoomHelper(context, videoRendering, core)
+    }
+
+    fun switchCamera() {
+        val currentDevice = core.videoDevice
+        Log.i(TAG, "[Context] Current camera device is $currentDevice")
+
+        for (camera in core.videoDevicesList) {
+            if (camera != currentDevice && camera != "StaticImage: Static picture") {
+                Log.i(TAG, "[Context] New camera device will be $camera")
+                core.videoDevice = camera
+                break
+            }
+        }
+
+        val conference = core.conference
+        if (conference == null || !conference.isIn) {
+            val call = core.currentCall
+            if (call == null) {
+                Log.w(TAG, "[Context] Switching camera while not in call")
+                return
+            }
+            call.update(null)
+        }
+    }
+
+
+    //初始化一些操作
+    private fun initLinphone() {
+
+        configureCore()
+
+        initUserCertificates()
+    }
+
+
+    private fun configureCore() {
+        // 来电铃声
+        //core.isNativeRingingEnabled = true
+        // 来电振动
+        //core.isVibrationOnIncomingCallEnabled = true
+        core.enableEchoCancellation(true) //回声消除
+        core.enableAdaptiveRateControl(true) //自适应码率控制
+
+        //音频部分,设置指定的音频格式.
+        val payloads: Array<PayloadType> = core.audioPayloadTypes
+        for (i in payloads.indices) {
+            val pt = payloads[i]
+            //Log.i("sipCall", ">>>>>>>>>>>>>>>>>1 " + pt.getMimeType() + " = " + pt.enabled());
+            if (pt.mimeType == "PCMU"
+                || pt.mimeType == "opus"
+                //|| pt.getMimeType().equals("PCMA")
+                //|| pt.getMimeType().equals("PUMA")
+                //|| pt.getMimeType().equals("GSM")
+            ) {
+                pt.enable(true)
+            } else {
+                pt.enable(false)
+            }
+            Log.i("sipCall", ">>>>>>>>>>>>>>>>>2 " + pt.mimeType + " = " + pt.enabled())
+        }
+        core.setAudioPayloadTypes(payloads)
+    }
+
+    private var gsmCallActive = false
+    private val phoneStateListener = object : PhoneStateListener() {
+        override fun onCallStateChanged(state: Int, phoneNumber: String?) {
+            gsmCallActive = when (state) {
+                TelephonyManager.CALL_STATE_OFFHOOK -> {
+                    Log.i(TAG, "[Context] Phone state is off hook")
+                    true
+                }
+                TelephonyManager.CALL_STATE_RINGING -> {
+                    Log.i(TAG, "[Context] Phone state is ringing")
+                    true
+                }
+                TelephonyManager.CALL_STATE_IDLE -> {
+                    Log.i(TAG, "[Context] Phone state is idle")
+                    false
+                }
+                else -> {
+                    Log.i(TAG, "[Context] Phone state is unexpected: $state")
+                    false
+                }
+            }
+        }
+    }
+
+
+    //设置存放用户x509证书的目录路径
+    private fun initUserCertificates() {
+        val userCertsPath = corePreferences!!.userCertificatesPath
+        val f = File(userCertsPath)
+        if (!f.exists()) {
+            if (!f.mkdir()) {
+                Log.e(TAG, "[Context] $userCertsPath can't be created.")
+            }
+        }
+        core.userCertificatesPath = userCertsPath
+    }
+
+
+    companion object {
+
+        // For Singleton instantiation
+        @SuppressLint("StaticFieldLeak")
+        @Volatile
+        private var instance: LinphoneManager? = null
+        var sipTesting = false
+
+        fun getInstance(context: Context) =
+            instance ?: synchronized(this) {
+                instance ?: LinphoneManager(context).also { instance = it }
+            }
+
+    }
+
+}

+ 23 - 22
android_door/src/main/common/java/com/wdkl/ncs/host/util/AudioRouteUtils.kt

@@ -1,13 +1,14 @@
-package com.wdkl.ncs.host.util
+package com.wdkl.ncs.host.sip.utils
 
-import android.telecom.CallAudioState
+import android.util.Log
 import org.linphone.core.AudioDevice
 import org.linphone.core.Call
 import org.linphone.core.Core
-import org.linphone.core.tools.Log
 
 class AudioRouteUtils {
     companion object {
+        val TAG = "AudioRoute"
+
         private fun applyAudioRouteChange(
             core: Core,
             call: Call?,
@@ -17,7 +18,7 @@ class AudioRouteUtils {
             val currentCall = if (core.callsNb > 0) {
                 call ?: core.currentCall ?: core.calls[0]
             } else {
-                Log.w("[Audio Route Helper] No call found, setting audio route on Core")
+                Log.w(TAG, "[Audio Route Helper] No call found, setting audio route on Core")
                 null
             }
             val conference = core.conference
@@ -26,18 +27,18 @@ class AudioRouteUtils {
             else
                 AudioDevice.Capabilities.CapabilityRecord
             val preferredDriver = if (output) {
-                core.defaultOutputAudioDevice?.driverName
+                core.defaultOutputAudioDevice.driverName
             } else {
-                core.defaultInputAudioDevice?.driverName
+                core.defaultInputAudioDevice.driverName
             }
 
             val extendedAudioDevices = core.extendedAudioDevices
-            Log.i("[Audio Route Helper] Looking for an ${if (output) "output" else "input"} audio device with capability [$capability], driver name [$preferredDriver] and type [$types] in extended audio devices list (size ${extendedAudioDevices.size})")
+            Log.i(TAG,"[Audio Route Helper] Looking for an ${if (output) "output" else "input"} audio device with capability [$capability], driver name [$preferredDriver] and type [$types] in extended audio devices list (size ${extendedAudioDevices.size})")
             val foundAudioDevice = extendedAudioDevices.find {
                 it.driverName == preferredDriver && types.contains(it.type) && it.hasCapability(capability)
             }
             val audioDevice = if (foundAudioDevice == null) {
-                Log.w("[Audio Route Helper] Failed to find an audio device with capability [$capability], driver name [$preferredDriver] and type [$types]")
+                Log.w(TAG,"[Audio Route Helper] Failed to find an audio device with capability [$capability], driver name [$preferredDriver] and type [$types]")
                 extendedAudioDevices.find {
                     types.contains(it.type) && it.hasCapability(capability)
                 }
@@ -46,23 +47,23 @@ class AudioRouteUtils {
             }
 
             if (audioDevice == null) {
-                Log.e("[Audio Route Helper] Couldn't find audio device with capability [$capability] and type [$types]")
+                Log.e(TAG,"[Audio Route Helper] Couldn't find audio device with capability [$capability] and type [$types]")
                 for (device in extendedAudioDevices) {
                     // TODO: switch to debug?
-                    Log.i("[Audio Route Helper] Extended audio device: [${device.deviceName} (${device.driverName}) ${device.type} / ${device.capabilities}]")
+                    Log.i(TAG,"[Audio Route Helper] Extended audio device: [${device.deviceName} (${device.driverName}) ${device.type} / ${device.capabilities}]")
                 }
                 return
             }
             if (conference != null && conference.isIn) {
-                Log.i("[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing conference audio to it")
+                Log.i(TAG,"[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing conference audio to it")
                 if (output) conference.outputAudioDevice = audioDevice
                 else conference.inputAudioDevice = audioDevice
             } else if (currentCall != null) {
-                Log.i("[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing call audio to it")
+                Log.i(TAG,"[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing call audio to it")
                 if (output) currentCall.outputAudioDevice = audioDevice
                 else currentCall.inputAudioDevice = audioDevice
             } else {
-                Log.i("[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], changing core default audio device")
+                Log.i(TAG,"[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], changing core default audio device")
                 if (output) core.outputAudioDevice = audioDevice
                 else core.inputAudioDevice = audioDevice
             }
@@ -72,22 +73,22 @@ class AudioRouteUtils {
             when (types.first()) {
                 AudioDevice.Type.Bluetooth -> {
                     if (isBluetoothAudioRecorderAvailable(core)) {
-                        Log.i("[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device")
+                        Log.i(TAG,"[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device")
                         applyAudioRouteChange(core, call, arrayListOf(AudioDevice.Type.Bluetooth), false)
                     }
                 }
                 AudioDevice.Type.Headset, AudioDevice.Type.Headphones -> {
                     if (isHeadsetAudioRecorderAvailable(core)) {
-                        Log.i("[Audio Route Helper] Headphones/Headset device is able to record audio, also change input audio device")
+                        Log.i(TAG,"[Audio Route Helper] Headphones/Headset device is able to record audio, also change input audio device")
                         applyAudioRouteChange(core, call, (arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset)), false)
                     }
                 }
                 AudioDevice.Type.Earpiece, AudioDevice.Type.Speaker -> {
-                    Log.i("[Audio Route Helper] Audio route requested to Earpiece or Speaker, setting input to Microphone")
+                    Log.i(TAG,"[Audio Route Helper] Audio route requested to Earpiece or Speaker, setting input to Microphone")
                     applyAudioRouteChange(core, call, (arrayListOf(AudioDevice.Type.Microphone)), false)
                 }
                 else -> {
-                    Log.w("[Audio Route Helper] Unexpected audio device type: ${types.first()}")
+                    Log.w(TAG,"[Audio Route Helper] Unexpected audio device type: ${types.first()}")
                 }
             }
         }
@@ -101,11 +102,11 @@ class AudioRouteUtils {
             val currentCall = call ?: core.currentCall ?: core.calls.firstOrNull()
 
                 if (currentCall != null) {
-                    Log.i("[Audio Route Helper] Telecom Helper & matching connection found, dispatching audio route change through it")
+                    Log.i(TAG,"[Audio Route Helper] Telecom Helper & matching connection found, dispatching audio route change through it")
                     // We will be called here again by NativeCallWrapper.onCallAudioStateChanged()
                     // but this time with skipTelecom = true
                     //if (!Compatibility.changeAudioRouteForTelecomManager(connection, route)) {
-                        Log.w("[Audio Route Helper] Connection is already using this route internally, make the change!")
+                        Log.w(TAG,"[Audio Route Helper] Connection is already using this route internally, make the change!")
                         applyAudioRouteChange(core, currentCall, types)
                         changeCaptureDeviceToMatchAudioRoute(core, currentCall, types)
                     //}
@@ -136,7 +137,7 @@ class AudioRouteUtils {
                 if (audioDevice.type == AudioDevice.Type.Bluetooth &&
                     audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)
                 ) {
-                    Log.i("[Audio Route Helper] Found bluetooth audio device [${audioDevice.deviceName} (${audioDevice.driverName})]")
+                    Log.i(TAG,"[Audio Route Helper] Found bluetooth audio device [${audioDevice.deviceName} (${audioDevice.driverName})]")
                     return true
                 }
             }
@@ -148,7 +149,7 @@ class AudioRouteUtils {
                 if (audioDevice.type == AudioDevice.Type.Bluetooth &&
                     audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityRecord)
                 ) {
-                    Log.i("[Audio Route Helper] Found bluetooth audio recorder [${audioDevice.deviceName} (${audioDevice.driverName})]")
+                    Log.i(TAG,"[Audio Route Helper] Found bluetooth audio recorder [${audioDevice.deviceName} (${audioDevice.driverName})]")
                     return true
                 }
             }
@@ -160,7 +161,7 @@ class AudioRouteUtils {
                 if ((audioDevice.type == AudioDevice.Type.Headset || audioDevice.type == AudioDevice.Type.Headphones) &&
                     audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityRecord)
                 ) {
-                    Log.i("[Audio Route Helper] Found headset/headphones audio recorder [${audioDevice.deviceName} (${audioDevice.driverName})]")
+                    Log.i(TAG,"[Audio Route Helper] Found headset/headphones audio recorder [${audioDevice.deviceName} (${audioDevice.driverName})]")
                     return true
                 }
             }

+ 171 - 0
android_door/src/main/common/java/com/wdkl/ncs/host/sip/utils/FileUtils.kt

@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.wdkl.ncs.host.sip.utils
+
+import android.content.Context
+import android.database.CursorIndexOutOfBoundsException
+import android.net.Uri
+import android.os.Environment
+import android.provider.OpenableColumns
+import android.webkit.MimeTypeMap
+import java.io.*
+import java.util.*
+
+import org.linphone.core.tools.Log
+
+class FileUtils {
+    companion object {
+        private val TAG = javaClass.simpleName
+
+        const val VFS_PLAIN_FILE_EXTENSION = ".bctbx_evfs_plain"
+
+        fun getNameFromFilePath(filePath: String): String {
+            var name = filePath
+            val i = filePath.lastIndexOf('/')
+            if (i > 0) {
+                name = filePath.substring(i + 1)
+            }
+            return name
+        }
+
+        fun getExtensionFromFileName(fileName: String): String {
+            val realFileName = if (fileName.endsWith(VFS_PLAIN_FILE_EXTENSION)) {
+                fileName.substring(0, fileName.length - VFS_PLAIN_FILE_EXTENSION.length)
+            } else fileName
+
+            var extension = MimeTypeMap.getFileExtensionFromUrl(realFileName)
+            if (extension.isNullOrEmpty()) {
+                val i = realFileName.lastIndexOf('.')
+                if (i > 0) {
+                    extension = realFileName.substring(i + 1)
+                }
+            }
+
+            return extension
+        }
+
+        fun isPlainTextFile(path: String): Boolean {
+            val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+            val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
+            return type?.startsWith("text/plain") ?: false
+        }
+
+        fun isExtensionPdf(path: String): Boolean {
+            val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+            val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
+            return type?.startsWith("application/pdf") ?: false
+        }
+
+        fun isExtensionImage(path: String): Boolean {
+            val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+            val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
+            return type?.startsWith("image/") ?: false
+        }
+
+        fun isExtensionVideo(path: String): Boolean {
+            val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+            val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
+            return type?.startsWith("video/") ?: false
+        }
+
+        fun isExtensionAudio(path: String): Boolean {
+            val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+            val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
+            return type?.startsWith("audio/") ?: false
+        }
+
+        fun getFileStorageDir(context: Context, isPicture: Boolean = false): File {
+            var path: File? = null
+            if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
+                Log.w("[File Utils] External storage is mounted")
+                var directory = Environment.DIRECTORY_DOWNLOADS
+                if (isPicture) {
+                    Log.w("[File Utils] Using pictures directory instead of downloads")
+                    directory = Environment.DIRECTORY_PICTURES
+                }
+                path = context.getExternalFilesDir(directory)
+            }
+
+            val returnPath: File = path ?: context.filesDir
+            if (path == null) Log.w("[File Utils] Couldn't get external storage path, using internal")
+
+            return returnPath
+        }
+
+        fun getFileStoragePath(context: Context, fileName: String): File {
+            val path = getFileStorageDir(context, isExtensionImage(fileName))
+            var file = File(path, fileName)
+
+            var prefix = 1
+            while (file.exists()) {
+                file = File(path, prefix.toString() + "_" + fileName)
+                Log.w("[File Utils] File with that name already exists, renamed to ${file.name}")
+                prefix += 1
+            }
+            return file
+        }
+
+        fun deleteFile(filePath: String) {
+            val file = File(filePath)
+            if (file.exists()) {
+                try {
+                    if (file.delete()) {
+                        Log.i("[File Utils] Deleted $filePath")
+                    } else {
+                        Log.e("[File Utils] Can't delete $filePath")
+                    }
+                } catch (e: Exception) {
+                    Log.e("[File Utils] Can't delete $filePath, exception: $e")
+                }
+            } else {
+                Log.e("[File Utils] File $filePath doesn't exists")
+            }
+        }
+
+
+        private fun getNameFromUri(uri: Uri, context: Context): String {
+            var name = ""
+            if (uri.scheme == "content") {
+                val returnCursor =
+                    context.contentResolver.query(uri, null, null, null, null)
+                if (returnCursor != null) {
+                    returnCursor.moveToFirst()
+                    val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
+                    if (nameIndex != -1) {
+                        try {
+                            name = returnCursor.getString(nameIndex)
+                        } catch (e: CursorIndexOutOfBoundsException) {
+                            Log.e("[File Utils] Failed to get the display name for URI $uri, exception is $e")
+                        }
+                    } else {
+                        Log.e("[File Utils] Couldn't get DISPLAY_NAME column index for URI: $uri")
+                    }
+                    returnCursor.close()
+                }
+            } else if (uri.scheme == "file") {
+                name = uri.lastPathSegment ?: ""
+            }
+            return name
+        }
+
+    }
+
+
+}

+ 106 - 0
android_door/src/main/common/java/com/wdkl/ncs/host/sip/utils/LinphoneUtils.kt

@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.wdkl.ncs.host.sip.utils
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkInfo
+import android.telephony.TelephonyManager.*
+import com.wdkl.ncs.host.sip.core.CorePreferences
+import java.text.DateFormat
+import java.text.SimpleDateFormat
+import java.util.*
+import org.linphone.core.*
+
+/**
+ * Various utility methods for Linphone SDK
+ */
+class LinphoneUtils {
+    companion object {
+        private const val RECORDING_DATE_PATTERN = "dd-MM-yyyy-HH-mm-ss"
+
+        fun getDisplayName(address: Address): String {
+            return address.displayName ?: address.username ?: ""
+        }
+
+        fun getDisplayableAddress(corePreferences: CorePreferences, address: Address?): String {
+            if (address == null) return "[null]"
+            return if (corePreferences.replaceSipUriByUsername) {
+                address.username ?: address.asStringUriOnly()
+            } else {
+                address.asStringUriOnly()
+            }
+        }
+
+        fun isLimeAvailable(core: Core): Boolean {
+            return core.limeX3DhAvailable() && core.limeX3DhEnabled() &&
+                    core.limeX3DhServerUrl != null &&
+                    core.defaultAccount?.params?.conferenceFactoryUri != null
+        }
+
+        fun isGroupChatAvailable(core: Core): Boolean {
+            return core.defaultAccount?.params?.conferenceFactoryUri != null
+        }
+
+
+        fun getRecordingFilePathForAddress(context: Context, address: Address): String {
+            val displayName = getDisplayName(address)
+            val dateFormat: DateFormat = SimpleDateFormat(
+                RECORDING_DATE_PATTERN,
+                Locale.getDefault()
+            )
+            val fileName = "${displayName}_${dateFormat.format(Date())}.mkv"
+            return FileUtils.getFileStoragePath(context, fileName).absolutePath
+        }
+
+        fun getRecordingDateFromFileName(name: String): Date {
+            return SimpleDateFormat(RECORDING_DATE_PATTERN, Locale.getDefault()).parse(name)
+        }
+
+        @SuppressLint("MissingPermission")
+        fun checkIfNetworkHasLowBandwidth(context: Context): Boolean {
+            val connMgr =
+                context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+            val networkInfo: NetworkInfo? = connMgr.activeNetworkInfo
+            if (networkInfo != null && networkInfo.isConnected) {
+                if (networkInfo.type == ConnectivityManager.TYPE_MOBILE) {
+                    return when (networkInfo.subtype) {
+                        NETWORK_TYPE_EDGE, NETWORK_TYPE_GPRS, NETWORK_TYPE_IDEN -> true
+                        else -> false
+                    }
+                }
+            }
+            // In doubt return false
+            return false
+        }
+
+        fun isCallLogMissed(callLog: CallLog): Boolean {
+            return (callLog.dir == Call.Dir.Incoming &&
+                    (callLog.status == Call.Status.Missed ||
+                            callLog.status == Call.Status.Aborted ||
+                            callLog.status == Call.Status.EarlyAborted))
+        }
+
+        fun getChatRoomId(localAddress: String, remoteAddress: String): String {
+            return "$localAddress~$remoteAddress"
+        }
+    }
+}

+ 148 - 0
android_door/src/main/common/java/com/wdkl/ncs/host/sip/utils/VideoZoomHelper.kt

@@ -0,0 +1,148 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.wdkl.ncs.host.sip.utils
+
+import android.content.Context
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.View
+import kotlin.math.max
+import kotlin.math.min
+import org.linphone.core.Call
+import org.linphone.core.Core
+
+class VideoZoomHelper(
+    context: Context,
+    private var videoDisplayView: View,
+    private val core: Core
+) : GestureDetector.SimpleOnGestureListener() {
+    private var scaleDetector: ScaleGestureDetector
+
+    private var zoomFactor = 1f
+    private var zoomCenterX = 0f
+    private var zoomCenterY = 0f
+
+    init {
+        val gestureDetector = GestureDetector(context, this)
+
+        scaleDetector = ScaleGestureDetector(context, object :
+            ScaleGestureDetector.SimpleOnScaleGestureListener() {
+            override fun onScale(detector: ScaleGestureDetector): Boolean {
+                zoomFactor *= detector.scaleFactor
+                // Don't let the object get too small or too large.
+                // Zoom to make the video fill the screen vertically
+                val portraitZoomFactor =
+                    videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
+                // Zoom to make the video fill the screen horizontally
+                val landscapeZoomFactor =
+                    videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
+                zoomFactor =
+                    max(0.1f, min(zoomFactor, max(portraitZoomFactor, landscapeZoomFactor)))
+
+                val currentCall: Call? = core.currentCall
+                if (currentCall != null) {
+                    currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
+                    return true
+                }
+
+                return false
+            }
+        })
+
+        videoDisplayView.setOnTouchListener { _, event ->
+            val currentZoomFactor = zoomFactor
+            scaleDetector.onTouchEvent(event)
+
+            if (currentZoomFactor != zoomFactor) {
+                // We did scale, prevent touch event from going further
+                return@setOnTouchListener true
+            }
+
+            // If true, gesture detected, prevent touch event from going further
+            // Otherwise it seems we didn't use event,
+            // allow it to be dispatched somewhere else
+            gestureDetector.onTouchEvent(event)
+        }
+    }
+
+    override fun onScroll(
+        e1: MotionEvent,
+        e2: MotionEvent,
+        distanceX: Float,
+        distanceY: Float
+    ): Boolean {
+        val currentCall: Call? = core.currentCall
+        if (currentCall != null) {
+            if (zoomFactor > 1) {
+                // Video is zoomed, slide is used to change center of zoom
+                if (distanceX > 0 && zoomCenterX < 1) {
+                    zoomCenterX += 0.01f
+                } else if (distanceX < 0 && zoomCenterX > 0) {
+                    zoomCenterX -= 0.01f
+                }
+
+                if (distanceY < 0 && zoomCenterY < 1) {
+                    zoomCenterY += 0.01f
+                } else if (distanceY > 0 && zoomCenterY > 0) {
+                    zoomCenterY -= 0.01f
+                }
+
+                if (zoomCenterX > 1) zoomCenterX = 1f
+                if (zoomCenterX < 0) zoomCenterX = 0f
+                if (zoomCenterY > 1) zoomCenterY = 1f
+                if (zoomCenterY < 0) zoomCenterY = 0f
+
+                currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
+                return true
+            }
+        }
+
+        return false
+    }
+
+    override fun onDoubleTap(e: MotionEvent?): Boolean {
+        val currentCall: Call? = core.currentCall
+        if (currentCall != null) {
+            if (zoomFactor == 1f) {
+                // Zoom to make the video fill the screen vertically
+                val portraitZoomFactor =
+                    videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
+                // Zoom to make the video fill the screen horizontally
+                val landscapeZoomFactor =
+                    videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
+                zoomFactor = max(portraitZoomFactor, landscapeZoomFactor)
+            } else {
+                resetZoom()
+            }
+
+            currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
+            return true
+        }
+
+        return false
+    }
+
+    private fun resetZoom() {
+        zoomFactor = 1f
+        zoomCenterY = 0.5f
+        zoomCenterX = zoomCenterY
+    }
+}

+ 0 - 20
android_door/src/main/common/res/raw/linphonerc_default

@@ -1,20 +0,0 @@
-[sip]
-contact="Linphone Android" <sip:allen@8.129.220.143>
-use_info=0
-use_ipv6=1
-keepalive_period=30000
-sip_port=-1
-sip_tcp_port=-1
-sip_tls_port=-1
-media_encryption=none
-
-[video]
-size=vga
-
-[app]
-tunnel=disabled
-push_notification=1
-
-[misc]
-max_calls=10
-history_max_size=100

+ 0 - 34
android_door/src/main/common/res/raw/linphonerc_factory

@@ -1,34 +0,0 @@
-
-#
-#This file shall not contain path referencing package name, in order to be portable when app is renamed.
-#Paths to resources must be set from LinphoneManager, after creating LinphoneCore.
-[net]
-mtu=1300
-#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"
-download_bw=0
-upload_bw=0
-force_ice_disablement=0
-
-[sip]
-guess_hostname=1
-register_only_when_network_is_up=1
-auto_net_state_mon=1
-auto_answer_replacing_calls=1
-ping_with_options=0
-use_cpim=1
-
-[video]
-displaytype=MSAndroidTextureDisplay
-
-[misc]
-enable_basic_to_client_group_chat_room_migration=0
-enable_simple_group_chat_message_state=0
-aggregate_imdn=1
-notify_each_friend_individually_when_presence_received=0
-
-[app]
-activation_code_length=4
-prefer_basic_chat_room=1
-
-[assistant]
-xmlrpc_url=https://subscribe.linphone.org:444/wizard.php

+ 7 - 3
android_door/src/main/h10_3128/AndroidManifest.xml

@@ -9,6 +9,9 @@
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
 
+    <!-- Needed for full screen intent in incoming call notifications -->
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
+
     <application
         android:allowBackup="true"
         android:label="@string/javashop_app_name"
@@ -28,10 +31,11 @@
         <activity android:name="com.wdkl.ncs.host.activity.SipTestActivity" />
         <activity android:name="com.wdkl.ncs.host.activity.CallActivity" />
 
-
         <service
-            android:name="com.wdkl.ncs.host.service.WdklSipService"
-            android:label="@string/javashop_app_name" />
+            android:name="com.wdkl.ncs.host.sip.core.LinCoreService"
+            android:foregroundServiceType="phoneCall|camera|microphone"
+            android:label="@string/app_name"
+            android:stopWithTask="false" />
 
         <provider
             android:name="androidx.core.content.FileProvider"

+ 71 - 44
android_door/src/main/h10_3128/java/com/wdkl/app/ncs/callingdoor/activity/CallingdoorActivity.kt

@@ -57,21 +57,24 @@ import com.wdkl.ncs.android.middleware.utils.AppUtil
 import com.wdkl.ncs.android.middleware.utils.CommonUtils
 import com.wdkl.ncs.android.middleware.utils.StringUtil
 import com.wdkl.ncs.android.middleware.utils.Util
-import com.wdkl.ncs.host.service.WdklSipService
+import com.wdkl.ncs.host.activity.CallActivity
+import com.wdkl.ncs.host.sip.callback.PhoneCallback
+import com.wdkl.ncs.host.sip.core.LinphoneManager
 import com.wdkl.ncs.janus.util.JanusConstant
 import kotlinx.android.synthetic.main.callingdoor_main_lay.*
-import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_home
+import kotlinx.android.synthetic.main.callingdoor_main_lay.ll_room_name
 import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_call
 import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_call_bed
+import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_home
+import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_more
 import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_nurse
+import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_power_reset
 import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_support
+import kotlinx.android.synthetic.main.callingdoor_main_lay.room_actions
 import kotlinx.android.synthetic.main.callingdoor_main_lay.room_cancel_call
-import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_more
-import kotlinx.android.synthetic.main.callingdoor_main_lay.room_action_power_reset
 import kotlinx.android.synthetic.main.callingdoor_main_lay.tv_room_name
 import kotlinx.android.synthetic.main.callingdoor_main_lay.view_flipper
-import kotlinx.android.synthetic.main.callingdoor_main_lay.room_actions
-import kotlinx.android.synthetic.main.callingdoor_main_lay.ll_room_name
+import kotlinx.android.synthetic.main.callingdoor_main_lay.app_version
 import kotlinx.android.synthetic.main.callingdoor_main_lay_rk3288.*
 import kotlinx.android.synthetic.main.view_bed_name.*
 import kotlinx.android.synthetic.main.view_title_layout.*
@@ -80,7 +83,7 @@ import okhttp3.Request
 import org.greenrobot.eventbus.EventBus
 import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
-import org.linphone.core.AccountCreator
+import org.linphone.core.Call
 import org.linphone.core.TransportType
 import serialporttest.utils.SerialPortUtil
 import serialporttest.utils.SerialPortUtil433
@@ -90,7 +93,6 @@ import java.util.*
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
 import kotlin.collections.ArrayList
-import kotlinx.android.synthetic.main.callingdoor_main_lay.app_version as app_version
 
 
 /**
@@ -139,7 +141,8 @@ class CallingdoorActivity :BaseActivity<CallingdoorActivityPresenter, Callingdoo
 
     private var trans433Data: Trans433Data? = null
 
-    private var mAccountCreator: AccountCreator? = null
+    private var linphoneManager: LinphoneManager? = null
+    private var sipReg: Boolean = false
 
     private lateinit var yfRk3288ApiManager: YF_RK3288_API_Manager
     private lateinit var zhylManager: ZhylManager
@@ -242,14 +245,60 @@ class CallingdoorActivity :BaseActivity<CallingdoorActivityPresenter, Callingdoo
         }
 
         if (SettingConfig.getSipEnabled(activity)) {
-            //启动sip服务
-            val serviceIntent = Intent(BaseApplication.appContext, WdklSipService::class.java)
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                //android8.0以上通过startForegroundService启动service
-                startForegroundService(serviceIntent)
-            } else {
-                startService(serviceIntent)
+            linphoneManager = LinphoneManager.getInstance(BaseApplication.appContext)
+            linphoneManager?.start()
+            linphoneManager?.phoneCallback = object : PhoneCallback() {
+                //通话状态回调监听
+                override fun incomingCall(call: Call) {
+                    linphoneManager?.answerCall(call, false)
+                }
+
+                override fun callConnected(call: Call) {
+                    if (LinphoneManager.sipTesting) {
+                        //SIP通话测试已建立完成,打开通话界面
+                        val intent = Intent(activity, CallActivity::class.java)
+                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                        startActivity(intent)
+                    } else {
+                        /*if (SettingConfig.getRecordEnable(BaseApplication.appContext)) {
+                            //通话录音
+                            call.params.recordFile = Environment.getExternalStorageDirectory().path +
+                                    "/" + Environment.DIRECTORY_DOWNLOADS +
+                                    "/" + TimeHandle.getRecTimeFilename() + ".wav"
+
+                            if (Constants.showCall && !call.isRecording) {
+                                call.startRecording();
+                            }
+
+                            Log.e("SipCall", "call recording: " + call.isRecording + ", path: " + call.params.recordFile)
+                        }*/
+
+                        EventBus.getDefault().post(MessageEvent("sip_connected", Constant.SIP_CONNECTED))
+                    }
+                }
+
+                override fun callEnd(call: Call) {
+                    /*if (SettingConfig.getRecordEnable(BaseApplication.appContext)) {
+                        Log.e("SipCall", "call recording: " + call.isRecording + ", record path: " + call.params.recordFile)
+                        if (call.isRecording) {
+                            call.stopRecording()
+                        }
+                    }*/
+
+                    EventBus.getDefault().post(MessageEvent("handoff", Constant.EVENT_END_CALL))
+                }
+
+                override fun callReleased(call: Call) {
+                    //
+                }
+
+                override fun error(string: String) {
+                    runOnUiThread {
+                        showMessage(string)
+                    }
+                }
             }
+
             view_title_layout_tv_point.text = "sip"
         } else {
             view_title_layout_tv_point.text = "rtc"
@@ -1206,34 +1255,12 @@ class CallingdoorActivity :BaseActivity<CallingdoorActivityPresenter, Callingdoo
 
 
         //配置sip账号并连接sip服务器
-        if (SettingConfig.getSipEnabled(activity)) {
-            //配置sip账户
-            if (WdklSipService.getCore() != null) {
-                mAccountCreator = WdklSipService.getCore().createAccountCreator(null)
-                // 以下三项必须
-                if (!TextUtils.isEmpty(Constant.SIP_ID) && !TextUtils.isEmpty(Constant.sip_ip)) {
-                    Log.e(TAG, "sip connect: ${Constant.SIP_ID}@${Constant.sip_ip}:${Constant.sip_port}")
-                    mAccountCreator!!.setDomain(Constant.sip_ip)
-                    mAccountCreator!!.setUsername(Constant.SIP_ID)
-                    mAccountCreator!!.setPassword(Constant.SIP_ID)
-                    //默认使用udp
-                    mAccountCreator!!.transport = TransportType.Udp
-
-                    // 这里会自动创建代理配置、认证信息到 SIP核心
-                    val cfg = mAccountCreator!!.createProxyConfig()
-                    // 确保新创建的是最新
-                    WdklSipService.getCore().defaultProxyConfig = cfg
-
-                    /*if (Constants.sip_port != null) {
-                        var transports = WdklSipService.getCore().transports
-                        transports.udpPort = Constants.sip_port!!
-                        transports.tcpPort = Constants.sip_port!!
-                        transports.tlsPort = -1
-                        WdklSipService.getCore().transports = transports
-                    }*/
-                } else {
-                    showMessage("SIP empty")
-                }
+        if (SettingConfig.getSipEnabled(activity) && !sipReg) {
+            if (!TextUtils.isEmpty(Constant.SIP_ID) && !TextUtils.isEmpty(Constant.sip_ip)){
+                linphoneManager?.createProxyConfig(Constant.SIP_ID, Constant.SIP_ID, "${Constant.sip_ip}:5060", TransportType.Udp)
+                sipReg = true
+            } else {
+                showMessage("SIP empty")
             }
         }
     }

+ 16 - 35
android_door/src/main/h10_3128/java/com/wdkl/app/ncs/callingdoor/fragment/SipCallFragment.kt

@@ -13,6 +13,7 @@ import com.google.gson.Gson
 import com.wdkl.app.ncs.callingdoor.R
 import com.wdkl.app.ncs.callingdoor.helper.DoorLightHelper
 import com.wdkl.app.ncs.callingdoor.helper.VoiceManagerUtil
+import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.lib.settings.SettingConfig
 import com.wdkl.ncs.android.lib.utils.AppTool
 import com.wdkl.ncs.android.lib.utils.RingPlayHelper
@@ -27,12 +28,10 @@ import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel
 import com.wdkl.ncs.android.middleware.tcp.enums.RoleTypeEnum
 import com.wdkl.ncs.android.middleware.tcp.enums.TcpAction
 import com.wdkl.ncs.android.middleware.tcp.enums.TcpType
-import com.wdkl.ncs.host.service.WdklSipService
-import com.wdkl.ncs.host.util.AudioRouteUtils
+import com.wdkl.ncs.host.sip.core.LinphoneManager
 import kotlinx.android.synthetic.main.sky_voice_call_layout.*
 import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
-import org.linphone.core.Core
 
 class SipCallFragment: BaseCallFragment() {
     private val TAG = "SipCallFragment"
@@ -46,7 +45,7 @@ class SipCallFragment: BaseCallFragment() {
     private var audioCall: Boolean = true
     private var callSuccess: Boolean = false
 
-    private var sipCore: Core? = null
+    private var linphoneManager: LinphoneManager? = null
     private var volume = 60
 
     private val handler = Handler(Looper.getMainLooper())
@@ -63,7 +62,7 @@ class SipCallFragment: BaseCallFragment() {
         //初始化计时器
         initCountDownTimer(sky_voice_call_timeout)
 
-        sipCore = WdklSipService.getCore()
+        linphoneManager = LinphoneManager.getInstance(BaseApplication.appContext)
 
         volume = SettingConfig.getExtensionCallVolume(activity)
         if (volume < 0 || volume > 100) {
@@ -72,7 +71,7 @@ class SipCallFragment: BaseCallFragment() {
         call_volume_bar.progress = volume/10
         tv_volume.text = "" + volume/10
         VoiceManagerUtil.setCallVoice(activity, volume)
-        sipCore?.isMicEnabled = true
+        linphoneManager?.enableMic(true)
 
         //tcp参数
         if (tcpModel != null) {
@@ -148,14 +147,14 @@ class SipCallFragment: BaseCallFragment() {
         }
 
         sky_voice_call_mute.setOnClickListener {
-            val micEnable = sipCore!!.isMicEnabled
+            val micEnable = linphoneManager?.micEnabled()
             Log.d(TAG,"mic enable: $micEnable")
 
-            if (micEnable) {
-                sipCore!!.isMicEnabled = false
+            if (micEnable == true) {
+                linphoneManager?.enableMic(false)
                 sky_voice_call_mute.isSelected = true
             } else {
-                sipCore!!.isMicEnabled = true
+                linphoneManager?.enableMic(true)
                 sky_voice_call_mute.isSelected = false
             }
         }
@@ -185,13 +184,7 @@ class SipCallFragment: BaseCallFragment() {
     }
 
     private fun callTerminate() {
-        if (sipCore != null && sipCore!!.callsNb > 0) {
-            var call = sipCore!!.currentCall
-            if (call == null) {
-                call = sipCore!!.calls[0]
-            }
-            call!!.terminate()
-        }
+        linphoneManager?.terminateCall()
     }
 
     override fun destroy() {
@@ -345,6 +338,7 @@ class SipCallFragment: BaseCallFragment() {
             return
         }
 
+        showMessage("Call connected!")
         if (audioOnly) {
             ll_voice_call.visibility = View.VISIBLE
         } else {
@@ -396,16 +390,9 @@ class SipCallFragment: BaseCallFragment() {
     }
 
     private fun toggleSpeaker(enable: Boolean) {
-        Log.d(TAG, "toggle speaker: $enable, sipCore: $sipCore")
-        if ( sipCore == null) {
-            return
-        }
+        Log.d(TAG, "toggle speaker: $enable")
 
-        if (enable) {
-            AudioRouteUtils.routeAudioToSpeaker(sipCore!!)
-        } else {
-            AudioRouteUtils.routeAudioToEarpiece(sipCore!!)
-        }
+        linphoneManager?.enableSpeaker(enable)
     }
 
     @Subscribe(threadMode = ThreadMode.MAIN)
@@ -424,7 +411,7 @@ class SipCallFragment: BaseCallFragment() {
                                 fromId = curTcpModel.fromId
                                 acceptCall()
 
-                                if (sipCore == null || TextUtils.isEmpty(curInteractionVO.toSipId)) {
+                                if (TextUtils.isEmpty(curInteractionVO.toSipId)) {
                                     //通话失败,重置并返回主界面
                                     showMessage("sip_core targetSipId empty!")
                                     Constant.CALL_STATE = Constant.CALL_STANDBY
@@ -433,13 +420,7 @@ class SipCallFragment: BaseCallFragment() {
                                     }
                                     callEnd(true)
                                 } else {
-                                    val addressToCall = sipCore!!.interpretUrl(curInteractionVO.toSipId)
-                                    val params = sipCore!!.createCallParams(null)
-                                    params?.isVideoEnabled = false
-                                    if (addressToCall != null) {
-                                        sipCore!!.inviteAddressWithParams(addressToCall, params!!)
-                                        Log.d(TAG, ">>>>>>>>>>> invite address: " + addressToCall.asString())
-                                    }
+                                    linphoneManager?.startCall(curInteractionVO.toSipId, false)
                                 }
                             } else if (curTcpModel.getAction() == TcpAction.VoiceAction.REJECT) {
                                 //我方呼出,对方拒绝
@@ -490,6 +471,7 @@ class SipCallFragment: BaseCallFragment() {
 
             Constant.EVENT_END_CALL -> {
                 Log.d(TAG, ">>>>>>>>>>>>>> EVENT_END_CALL")
+                showMessage("Call end!")
                 if (messageEvent.getMessage() is String) {
                     val str = messageEvent.message as String
                     if (str.equals("cancel")) {
@@ -502,7 +484,6 @@ class SipCallFragment: BaseCallFragment() {
             }
 
             Constant.SIP_CONNECTED -> {
-                sipCore!!.isMicEnabled = true
                 showCalling(true)
             }
         }

+ 0 - 7
android_door/src/main/h10_3128/java/com/wdkl/app/ncs/callingdoor/helper/AppUpdateHelper.java

@@ -20,7 +20,6 @@ import com.wdkl.app.ncs.callingdoor.BuildConfig;
 import com.wdkl.ncs.android.component.welcome.activity.WelcomeActivity;
 import com.wdkl.ncs.android.lib.base.BaseApplication;
 import com.wdkl.ncs.android.lib.settings.SettingConfig;
-import com.wdkl.ncs.host.service.WdklSipService;
 
 import java.io.BufferedReader;
 import java.io.File;
@@ -274,12 +273,6 @@ public class AppUpdateHelper {
     }
 
     public static void restartApp(Context context) {
-        if (SettingConfig.getSipEnabled(context)) {
-            //停止sip服务
-            Intent serviceIntent = new Intent(context, WdklSipService.class);
-            context.stopService(serviceIntent);
-        }
-
         //重新启动app
         Intent mStartActivity = new Intent(context.getApplicationContext(), WelcomeActivity.class);
         mStartActivity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);

+ 1 - 1
common/build.gradle

@@ -249,7 +249,7 @@ dependencies {
     compile 'com.google.guava:guava:23.0'
 
     //linphone sip sdk
-    api(name: 'linphone-sdk-android-5.2.10', ext: 'aar')
+    api(name: 'linphone-sdk-android-5.0.71', ext: 'aar')
 }
 
 //tasks.withType(JavaCompile) {

BIN=BIN
common/libs/linphone-sdk-android-5.2.10.aar