فهرست منبع

西语项目增加sip通话功能

weizhengliang 11 ماه پیش
والد
کامیت
fe8d27c08f
49فایلهای تغییر یافته به همراه3778 افزوده شده و 737 حذف شده
  1. 34 0
      android_mobile/src/main/assets/assistant_default_values
  2. 40 0
      android_mobile/src/main/assets/assistant_linphone_default_values
  3. 42 0
      android_mobile/src/main/assets/linphonerc_default
  4. 43 0
      android_mobile/src/main/assets/linphonerc_factory
  5. 10 21
      android_mobile/src/main/common/java/com/wdkl/ncs/host/activity/CallActivity.java
  6. 7 12
      android_mobile/src/main/common/java/com/wdkl/ncs/host/activity/SipTestActivity.kt
  7. 0 338
      android_mobile/src/main/common/java/com/wdkl/ncs/host/service/WdklSipService.java
  8. 45 0
      android_mobile/src/main/common/java/com/wdkl/ncs/host/sip/callback/PhoneCallback.java
  9. 13 0
      android_mobile/src/main/common/java/com/wdkl/ncs/host/sip/callback/RegistrationCallback.java
  10. 540 0
      android_mobile/src/main/common/java/com/wdkl/ncs/host/sip/core/CorePreferences.kt
  11. 248 0
      android_mobile/src/main/common/java/com/wdkl/ncs/host/sip/core/LinCoreService.java
  12. 547 0
      android_mobile/src/main/common/java/com/wdkl/ncs/host/sip/core/LinphoneManager.kt
  13. 23 21
      android_mobile/src/main/common/java/com/wdkl/ncs/host/util/AudioRouteUtils.kt
  14. 171 0
      android_mobile/src/main/common/java/com/wdkl/ncs/host/sip/utils/FileUtils.kt
  15. 106 0
      android_mobile/src/main/common/java/com/wdkl/ncs/host/sip/utils/LinphoneUtils.kt
  16. 148 0
      android_mobile/src/main/common/java/com/wdkl/ncs/host/sip/utils/VideoZoomHelper.kt
  17. 0 20
      android_mobile/src/main/common/res/raw/linphonerc_default
  18. 0 34
      android_mobile/src/main/common/res/raw/linphonerc_factory
  19. 0 1
      android_mobile/src/main/yd_w_xiaomi_2/code/com/wdkl/ncs/android/component/home/ui/BaseSipCallFragment.java
  20. 0 1
      android_mobile/src/main/yd_w_xiaomi_2/code/com/wdkl/ncs/android/component/home/ui/FragmentSipAudio.java
  21. 13 0
      android_mobile/src/main/yd_w_xiaomi_2_chile/AndroidManifest.xml
  22. 7 1
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/NewCallListActivity.kt
  23. 9 1
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/WatchCallRecordsActivity.kt
  24. 10 2
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/WatchContactsActivity.kt
  25. 112 18
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/WatchHome2Activity.kt
  26. 2 0
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/WatchUserSettingActivity.java
  27. 15 3
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/NewEventItemAdapter.kt
  28. 9 1
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/RoomItemAdapter.kt
  29. 7 1
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/TakeoverItemAdapter.kt
  30. 9 1
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/TakeoverItemSearchAdapter.kt
  31. 8 1
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/WatchContactsItemAdapter.kt
  32. 22 25
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/service/FloatingService.java
  33. 13 2
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/service/WdKeepAliveService.kt
  34. 35 0
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/BaseCallActivity.java
  35. 399 0
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/BaseSipCallFragment.java
  36. 2 21
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/CallSingleActivity.java
  37. 0 46
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/FragmentAudio.java
  38. 422 0
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/FragmentSipAudio.java
  39. 1 73
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/SingleCallFragment.java
  40. 366 0
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/SipCallActivity.java
  41. 4 4
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/util/ActivityStackUtil.java
  42. 0 64
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/util/HandleTcpConnect.kt.bak
  43. 246 0
      android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/util/VoiceManagerUtil.java
  44. 42 20
      android_mobile/src/main/yd_w_xiaomi_2_chile/res/layout/user_setting_layout.xml
  45. 0 1
      android_mobile/src/main/yd_watch_2/code/com/wdkl/ncs/android/component/home/ui/BaseSipCallFragment.java
  46. 0 1
      android_mobile/src/main/yd_watch_2/code/com/wdkl/ncs/android/component/home/ui/FragmentSipAudio.java
  47. 7 2
      app/src/main/AndroidManifest.xml
  48. 1 1
      common/build.gradle
  49. BIN
      common/libs/linphone-sdk-android-5.2.10.aar

+ 34 - 0
android_mobile/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_mobile/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_mobile/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_mobile/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

+ 10 - 21
android_mobile/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.ncs.android.component.home.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,7 @@ 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 +46,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 +71,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 +89,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 +101,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 +142,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_mobile/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.ncs.android.component.home.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 - 338
android_mobile/src/main/common/java/com/wdkl/ncs/host/service/WdklSipService.java

@@ -1,338 +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.ncs.android.component.home.R;
-import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig;
-import com.wdkl.ncs.android.component.home.util.ActivityStackUtil;
-import com.wdkl.ncs.android.lib.base.BaseApplication;
-import com.wdkl.ncs.android.lib.utils.ExtendMethodsKt;
-import com.wdkl.ncs.android.lib.vo.MessageEvent;
-import com.wdkl.ncs.android.middleware.common.Constants;
-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.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();
-                ExtendMethodsKt.showMessage(message);
-                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();
-                    //如果在通话界面将自动接听,否则挂断
-                    if (ActivityStackUtil.getCallSingleActivity() != null) {
-                        CallParams params = getCore().createCallParams(call);
-                        call.acceptWithParams(params);
-                    } else {
-                        call.terminate();
-                    }
-                } 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_connect", Constants.SIP_CONNECTED));
-                    }
-                } else if (state == Call.State.End || state == Call.State.Released){
-                    EventBus.getDefault().post(new MessageEvent("handoff", Constants.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, Constants.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) {
-        Log.e("sip", "onTaskRemoved");
-        // 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_mobile/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_mobile/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_mobile/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.ncs.android.component.home.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_mobile/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");
+        }
+
+    }
+}

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

@@ -0,0 +1,547 @@
+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)
+        //Set custom sip port
+        //val transports = core.transports
+        //transports.udpPort = 5060
+        //transports.tcpPort = 5060
+        //core.transports = transports
+    }
+
+    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) {
+        Log.e(TAG, "audio enable speaker: $speakerEnabled")
+        if (speakerEnabled) {
+            AudioRouteUtils.routeAudioToSpeaker(core)
+        } else {
+            if ("A01".equals(Build.MODEL)) {
+                //耳机模式
+                AudioRouteUtils.routeAudioToHeadset(core)
+            } else {
+                AudioRouteUtils.routeAudioToEarpiece(core)
+            }
+        }
+
+        //Log.e(TAG, "input device: " + core.defaultInputAudioDevice.type + ", output device: " + core.defaultOutputAudioDevice.type)
+    }
+
+    //切换到耳机
+    fun routeToHeadset() {
+        //耳机模式
+        AudioRouteUtils.routeAudioToHeadset(core)
+    }
+
+    //是否在通话中
+    fun isCallConnected(): Boolean {
+        if (core.callsNb > 0) {
+            var call = core.currentCall
+            if (call == null) {
+                call = core.calls[0]
+            }
+
+            if (call?.state == Call.State.Connected || call?.state == Call.State.StreamsRunning) {
+                return true
+            }
+        }
+
+        return false
+    }
+
+    /**
+     * 是否是视频电话
+     * @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 - 21
android_mobile/src/main/common/java/com/wdkl/ncs/host/util/AudioRouteUtils.kt

@@ -1,12 +1,14 @@
-package com.wdkl.ncs.host.util
+package com.wdkl.ncs.host.sip.utils
 
+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?,
@@ -16,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
@@ -25,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)
                 }
@@ -45,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
             }
@@ -71,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()}")
                 }
             }
         }
@@ -100,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)
                     //}
@@ -135,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
                 }
             }
@@ -147,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
                 }
             }
@@ -159,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_mobile/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_mobile/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_mobile/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_mobile/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_mobile/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

+ 0 - 1
android_mobile/src/main/yd_w_xiaomi_2/code/com/wdkl/ncs/android/component/home/ui/BaseSipCallFragment.java

@@ -31,7 +31,6 @@ import com.wdkl.ncs.android.component.home.R;
 import com.wdkl.ncs.android.component.home.util.MediaPlayHelper;
 import com.wdkl.ncs.android.component.home.util.RingPlayHelper;
 import com.wdkl.ncs.android.component.home.util.SpeechUtil;
-import com.wdkl.ncs.android.component.home.util.VoiceManagerUtil;
 import com.wdkl.ncs.android.lib.base.BaseApplication;
 import com.wdkl.ncs.android.lib.utils.ExtendMethodsKt;
 import com.wdkl.ncs.android.lib.vo.MessageEvent;

+ 0 - 1
android_mobile/src/main/yd_w_xiaomi_2/code/com/wdkl/ncs/android/component/home/ui/FragmentSipAudio.java

@@ -24,7 +24,6 @@ import com.wdkl.ncs.android.component.home.util.MediaPlayHelper;
 import com.wdkl.ncs.android.component.home.util.RingPlayHelper;
 import com.wdkl.ncs.android.component.home.util.SpeechUtil;
 import com.wdkl.ncs.android.component.home.util.Util;
-import com.wdkl.ncs.android.component.home.util.VoiceManagerUtil;
 import com.wdkl.ncs.android.lib.utils.ExtendMethodsKt;
 import com.wdkl.ncs.android.middleware.common.Constants;
 import com.wdkl.ncs.android.middleware.model.vo.InteractionVO;

+ 13 - 0
android_mobile/src/main/yd_w_xiaomi_2_chile/AndroidManifest.xml

@@ -81,6 +81,19 @@
             </intent-filter>
         </activity>
 
+        <activity
+            android:name=".ui.SipCallActivity"
+            android:showOnLockScreen="true"
+            android:showWhenLocked="true"
+            android:excludeFromRecents="true"
+            android:screenOrientation="portrait"
+            android:configChanges="keyboardHidden|orientation|screenSize"
+            android:launchMode="singleInstance">
+            <intent-filter>
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
         <service android:name=".service.FloatingService"/>
 
         <service android:name="com.wdkl.ncs.android.component.home.service.WdKeepAliveService">

+ 7 - 1
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/NewCallListActivity.kt

@@ -22,6 +22,7 @@ import com.wdkl.ncs.android.component.home.adapter.NewCallItemAdapter
 import com.wdkl.ncs.android.component.home.service.WdKeepAliveService
 import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
 import com.wdkl.ncs.android.component.home.util.RingPlayHelper
 import com.wdkl.ncs.android.component.home.util.SpeechUtil
 import com.wdkl.ncs.android.lib.base.BaseApplication
@@ -116,7 +117,12 @@ class NewCallListActivity : BaseToolActivity(), NewCallItemAdapter.CallClickList
                 Log.i(TAG, "来电:" + JSON.toJSONString(data))
 
                 //启动 activity
-                var intent = Intent(activity, CallSingleActivity::class.java)
+                val intent: Intent
+                if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                    intent = Intent(this, SipCallActivity::class.java)
+                } else {
+                    intent = Intent(this, CallSingleActivity::class.java)
+                }
                 intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, roomId)
                 intent.putExtra(CallSingleActivity.EXTRA_MO, false)
                 intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)

+ 9 - 1
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/WatchCallRecordsActivity.kt

@@ -17,7 +17,10 @@ import com.wdkl.ncs.android.component.home.R
 import com.wdkl.ncs.android.component.home.adapter.WatchCallRecordsItemAdapter
 import com.wdkl.ncs.android.component.home.databinding.WatchActivityCallRecordsBinding
 import com.wdkl.ncs.android.component.home.launch.HomeLaunch
+import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
+import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.middleware.common.Constants
 import com.wdkl.ncs.android.lib.utils.showMessage
 import com.wdkl.ncs.android.middleware.logic.contract.home.WatchCallRecordsFragmentContract
@@ -162,7 +165,12 @@ class WatchCallRecordsActivity : BaseActivity<WatchCallRecordsFragmentPresenter,
                         DeviceChannel.calling = true
                         val tcpModel = VoiceUtil.voiceCall(Constants.deviceId, receivedData!!.deviceId)
                         //启动通话界面
-                        val intent = Intent(this@WatchCallRecordsActivity, CallSingleActivity::class.java)
+                        val intent: Intent
+                        if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                            intent = Intent(this@WatchCallRecordsActivity, SipCallActivity::class.java)
+                        } else {
+                            intent = Intent(this@WatchCallRecordsActivity, CallSingleActivity::class.java)
+                        }
                         intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, Constants.sipId)
                         intent.putExtra(CallSingleActivity.EXTRA_MO, true)
                         intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)

+ 10 - 2
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/WatchContactsActivity.kt

@@ -14,7 +14,10 @@ import com.wdkl.ncs.android.component.home.adapter.RoomItemAdapter
 import com.wdkl.ncs.android.component.home.adapter.WatchContactsItemAdapter
 import com.wdkl.ncs.android.component.home.databinding.WatchContactsLayBinding
 import com.wdkl.ncs.android.component.home.launch.HomeLaunch
+import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
+import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.middleware.common.Constants
 import com.wdkl.ncs.android.lib.utils.showMessage
 import com.wdkl.ncs.android.middleware.logic.contract.home.WatchActivityContract
@@ -173,7 +176,12 @@ class WatchContactsActivity : BaseActivity<WatchActivityPresenter, WatchContacts
                         if (device.frameId == contact.frameParentId) {
                             //发起呼叫
                             val tcpModel = VoiceUtil.voiceCall(Constants.deviceId, device.id)
-                            val intent = Intent(activity, CallSingleActivity::class.java)
+                            val intent: Intent
+                            if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                                intent = Intent(activity, SipCallActivity::class.java)
+                            } else {
+                                intent = Intent(activity, CallSingleActivity::class.java)
+                            }
                             intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, Constants.sipId)
                             intent.putExtra(CallSingleActivity.EXTRA_MO, true)
                             intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)
@@ -186,7 +194,7 @@ class WatchContactsActivity : BaseActivity<WatchActivityPresenter, WatchContacts
                         }
                     }
                 } else {
-                    showMessage("没有设备")
+                    showMessage("No device")
                 }
             }
         })

+ 112 - 18
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/WatchHome2Activity.kt

@@ -49,6 +49,9 @@ import com.wdkl.ncs.android.middleware.tcp.enums.CommunicationEnum
 import com.wdkl.ncs.android.middleware.tcp.enums.RoleTypeEnum
 import com.wdkl.ncs.android.middleware.utils.CommonUtils
 import com.wdkl.ncs.android.middleware.utils.ContactHelper
+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.keepbackground.utils.SpManager
 import com.wdkl.ncs.keepbackground.work.DaemonEnv
 import com.wdkl.rtc.util.JanusConstant
@@ -58,6 +61,8 @@ import kotlinx.android.synthetic.main.watch_activity_register.*
 import org.greenrobot.eventbus.EventBus
 import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
+import org.linphone.core.Call
+import org.linphone.core.TransportType
 import java.util.*
 import kotlin.collections.ArrayList
 
@@ -89,6 +94,9 @@ class WatchHome2Activity : BaseActivity<WatchHomeActivityPresenter, WatchActivit
 
     private var tcpInit = false
 
+    private var linphoneManager: LinphoneManager? = null
+    private var sipReg: Boolean = false
+
     override fun onCreate(savedInstanceState: Bundle?) {
         if (!isTaskRoot) {
             finish()
@@ -122,6 +130,67 @@ class WatchHome2Activity : BaseActivity<WatchHomeActivityPresenter, WatchActivit
 
         initCountDownTimer()
 
+        if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+            linphoneManager = LinphoneManager.getInstance(BaseApplication.appContext)
+            linphoneManager?.start()
+            linphoneManager?.phoneCallback = object : PhoneCallback() {
+                //通话状态回调监听
+                override fun incomingCall(call: Call) {
+                    //如果在通话界面将自动接听,否则挂断
+                    if (ActivityStackUtil.getCallSingleActivity() != null) {
+                        linphoneManager?.answerCall(call, false)
+                    } else {
+                        call.terminate()
+                    }
+                }
+
+                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_connect", Constants.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", Constants.EVENT_END_CALL))
+                }
+
+                override fun callReleased(call: Call) {
+                    //
+                }
+
+                override fun error(string: String) {
+                    runOnUiThread {
+                        showMessage(string)
+                    }
+                }
+            }
+        }
+
         //网络强度监听
         teleManager = (applicationContext.getSystemService(Context.TELEPHONY_SERVICE)) as TelephonyManager
         netType = NetHelper.getInstance().getNetworkState(applicationContext)
@@ -286,23 +355,6 @@ class WatchHome2Activity : BaseActivity<WatchHomeActivityPresenter, WatchActivit
      * 返回的tcp信息
      */
     override fun setTcpServerHost(tcpSeverDTO: TcpSeverDTO) {
-        /*Log.d(TAG, "获取服务器IP完成")
-        if (tcpSeverDTO.publicIp != null && tcpSeverDTO.tcpPort != null && tcpSeverDTO.readerIdleTime != null) {
-            Constants.tcpServer = tcpSeverDTO.publicIp
-            Constants.tcpPort = tcpSeverDTO.tcpPort
-            Constants.heartBeat = tcpSeverDTO.readerIdleTime
-            tv_server_ip.text = tcpSeverDTO.publicIp
-
-            //成功获取到数据后再启动keepService并连接tcp服务器
-            if (DaemonEnv.app == null && !Strings.isNullOrEmpty(Constants.tcpServer) && !WdKeepAliveService.instanceCreated) {
-                Log.d(TAG, "开始 WdKeepAliveService")
-                //保活守护进程
-                DaemonEnv.init(this)
-                DaemonEnv.startServiceSafely(this, WdKeepAliveService::class.java)
-            }
-
-            presenter.getDeviceVO(Constants.imei)
-        }*/
     }
 
     override fun setServerInfo(serverIpInfo: ServerIpInfo) {
@@ -324,6 +376,36 @@ class WatchHome2Activity : BaseActivity<WatchHomeActivityPresenter, WatchActivit
                 DaemonEnv.startServiceSafely(this, WdKeepAliveService::class.java)
             }*/
 
+            if (serverIpInfo.sipPublicIp != null) {
+                Constants.sip_ip = serverIpInfo.sipPublicIp
+            }
+            if (serverIpInfo.sipPublicPort != null) {
+                Constants.sip_port = serverIpInfo.sipPublicPort
+            }
+
+            if (serverIpInfo.voiceType != null) {
+                val orgSipEnable = SettingConfig.getSipEnabled(activity)
+                if ("sip".equals(serverIpInfo.voiceType)) {
+                    //使用sip通话
+                    Constants.voice_type = "sip"
+                    SettingConfig.setSipEnable(activity, true)
+                    if (!orgSipEnable) {
+                        AppTool.Time.delay(8000) {
+                            AppUtils.restartApp()
+                        }
+                    }
+                } else {
+                    //使用webrtc通话
+                    Constants.voice_type = "webrtc"
+                    SettingConfig.setSipEnable(activity, false)
+                    if (orgSipEnable) {
+                        AppTool.Time.delay(8000) {
+                            AppUtils.restartApp()
+                        }
+                    }
+                }
+            }
+
             presenter.getDeviceVO(Constants.imei)
         }
 
@@ -451,9 +533,11 @@ class WatchHome2Activity : BaseActivity<WatchHomeActivityPresenter, WatchActivit
             return
         }
 
+        if (data.sipId != null) {
+            Constants.sipId = data.sipId
+        }
         Constants.partId = data.partId
         Constants.deviceId = data.id
-        Constants.sipId = data.sipId
         Constants.deviceType = data.deviceType
 
         if (DaemonEnv.app == null && !WdKeepAliveService.instanceCreated) {
@@ -620,6 +704,16 @@ class WatchHome2Activity : BaseActivity<WatchHomeActivityPresenter, WatchActivit
         if (partSettingDO.communicationModeNurse != null) {
             Constants.nursePhoneType = partSettingDO.communicationModeNurse
         }
+
+        //配置sip账号并连接sip服务器
+        if (SettingConfig.getSipEnabled(activity) && !sipReg) {
+            if (!TextUtils.isEmpty(Constants.sipId) && !TextUtils.isEmpty(Constants.sip_ip)) {
+                linphoneManager?.createProxyConfig(Constants.sipId!!, Constants.sipId!!, "${Constants.sip_ip}:5060", TransportType.Udp)
+                sipReg = true
+            } else {
+                showMessage("SIP empty")
+            }
+        }
     }
 
     fun onTcpConnectSuccess() {

+ 2 - 0
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/activity/WatchUserSettingActivity.java

@@ -99,6 +99,8 @@ public class WatchUserSettingActivity extends Activity {
         tvServerIp.setText(Constants.Companion.getTcpServer());
         String type = getString(R.string.phone_type, Constants.Companion.getPhoneType(), Constants.Companion.getBedPhoneType(), Constants.Companion.getNursePhoneType());
         tvPhoneType.setText(type);
+        TextView tvVoiceMode = findViewById(R.id.tv_voice_type);
+        tvVoiceMode.setText(Constants.Companion.getVoice_type());
 
         if (SettingConfig.getVoiceCallType(BaseApplication.appContext) == SettingConfig.PHONE_CALL) {
             callYes.setChecked(true);

+ 15 - 3
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/NewEventItemAdapter.kt

@@ -16,10 +16,13 @@ import com.google.gson.Gson
 import com.wdkl.ncs.android.component.home.R
 import com.wdkl.ncs.android.component.home.activity.VoiceMsgActivity
 import com.wdkl.ncs.android.component.home.databinding.EventListItemBinding
+import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
 import com.wdkl.ncs.android.component.home.util.*
 import com.wdkl.ncs.android.middleware.common.Constants
 import com.wdkl.ncs.android.lib.adapter.BaseDelegateAdapter
+import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.lib.utils.BaseRecyclerViewHolder
 import com.wdkl.ncs.android.lib.utils.showMessage
 import com.wdkl.ncs.android.middleware.api.ApiManager
@@ -287,7 +290,12 @@ class NewEventItemAdapter(var data:ArrayList<InteractionVO>, val activity: Activ
                     DeviceChannel.calling = true
 
                     val tcpModel = VoiceUtil.voiceCall(Constants.deviceId, itemData.toDeviceId)
-                    val intent = Intent(activity, CallSingleActivity::class.java)
+                    val intent: Intent
+                    if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                        intent = Intent(activity, SipCallActivity::class.java)
+                    } else {
+                        intent = Intent(activity, CallSingleActivity::class.java)
+                    }
                     intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, Constants.sipId)
                     intent.putExtra(CallSingleActivity.EXTRA_MO, true)
                     intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)
@@ -299,8 +307,12 @@ class NewEventItemAdapter(var data:ArrayList<InteractionVO>, val activity: Activ
                 } else {
                     DeviceChannel.calling = true
                     val tcpModel = VoiceUtil.voiceCall(Constants.deviceId, itemData.fromDeviceId)
-
-                    val intent = Intent(activity, CallSingleActivity::class.java)
+                    val intent: Intent
+                    if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                        intent = Intent(activity, SipCallActivity::class.java)
+                    } else {
+                        intent = Intent(activity, CallSingleActivity::class.java)
+                    }
                     intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, Constants.sipId)
                     intent.putExtra(CallSingleActivity.EXTRA_MO, true)
                     intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)

+ 9 - 1
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/RoomItemAdapter.kt

@@ -12,9 +12,12 @@ import com.alibaba.fastjson.JSONObject
 import com.wdkl.ncs.android.component.home.R
 import com.wdkl.ncs.android.component.home.activity.VoiceMsgActivity
 import com.wdkl.ncs.android.component.home.databinding.AdapterRoomItemBinding
+import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
 import com.wdkl.ncs.android.middleware.common.Constants
 import com.wdkl.ncs.android.lib.adapter.BaseDelegateAdapter
+import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.lib.utils.BaseRecyclerViewHolder
 import com.wdkl.ncs.android.lib.utils.showMessage
 import com.wdkl.ncs.android.middleware.model.vo.DeviceVO
@@ -82,7 +85,12 @@ class RoomItemAdapter(val data:ArrayList<DeviceVO>, val context: Context) : Base
                     DeviceChannel.calling = true
                     //通话
                     val tcpModel = VoiceUtil.voiceCall(Constants.deviceId, itemData.id)
-                    val intent = Intent(context, CallSingleActivity::class.java)
+                    val intent: Intent
+                    if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                        intent = Intent(context, SipCallActivity::class.java)
+                    } else {
+                        intent = Intent(context, CallSingleActivity::class.java)
+                    }
                     intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, Constants.sipId)
                     intent.putExtra(CallSingleActivity.EXTRA_MO, true)
                     intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)

+ 7 - 1
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/TakeoverItemAdapter.kt

@@ -19,6 +19,7 @@ import com.wdkl.ncs.android.component.home.activity.VoiceMsgActivity
 import com.wdkl.ncs.android.component.home.databinding.TakeoverItemBinding
 import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
 import com.wdkl.ncs.android.middleware.common.Constants
 import com.wdkl.ncs.android.lib.adapter.BaseDelegateAdapter
 import com.wdkl.ncs.android.lib.base.BaseApplication
@@ -149,7 +150,12 @@ class TakeoverItemAdapter(var data:ArrayList<JsonObject>, val activity: Activity
                     } else {
                         //网络电话
                         val tcpModel = VoiceUtil.voiceCall(Constants.deviceId, itemData.get("id").asInt)
-                        val intent = Intent(activity, CallSingleActivity::class.java)
+                        val intent: Intent
+                        if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                            intent = Intent(activity, SipCallActivity::class.java)
+                        } else {
+                            intent = Intent(activity, CallSingleActivity::class.java)
+                        }
                         intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, Constants.sipId)
                         intent.putExtra(CallSingleActivity.EXTRA_MO, true)
                         intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)

+ 9 - 1
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/TakeoverItemSearchAdapter.kt

@@ -21,7 +21,10 @@ import com.wdkl.ncs.android.component.home.activity.VoiceMsgActivity
 import com.wdkl.ncs.android.component.home.entity.ContactItemEntity
 import com.wdkl.ncs.android.component.home.search.FuzzySearchBaseAdapter
 import com.wdkl.ncs.android.component.home.search.IFuzzySearchRule
+import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
+import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.lib.utils.showMessage
 import com.wdkl.ncs.android.middleware.common.Constants
 import com.wdkl.ncs.android.middleware.tcp.TcpClient
@@ -141,7 +144,12 @@ class TakeoverItemSearchAdapter : FuzzySearchBaseAdapter<ContactItemEntity, Take
                 } else {
                     //网络电话
                     val tcpModel = VoiceUtil.voiceCall(Constants.deviceId, itemData.id)
-                    val intent = Intent(activity, CallSingleActivity::class.java)
+                    val intent: Intent
+                    if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                        intent = Intent(activity, SipCallActivity::class.java)
+                    } else {
+                        intent = Intent(activity, CallSingleActivity::class.java)
+                    }
                     intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, Constants.sipId)
                     intent.putExtra(CallSingleActivity.EXTRA_MO, true)
                     intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)

+ 8 - 1
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/adapter/WatchContactsItemAdapter.kt

@@ -17,8 +17,10 @@ import com.wdkl.ncs.android.component.home.activity.VoiceMsgActivity
 import com.wdkl.ncs.android.component.home.databinding.AdapterWatchContactsItemBinding
 import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
 import com.wdkl.ncs.android.middleware.common.Constants
 import com.wdkl.ncs.android.lib.adapter.BaseDelegateAdapter
+import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.lib.utils.BaseRecyclerViewHolder
 import com.wdkl.ncs.android.lib.utils.showMessage
 import com.wdkl.ncs.android.middleware.model.vo.WatchContactsVO
@@ -108,7 +110,12 @@ class WatchContactsItemAdapter(val data:ArrayList<WatchContactsVO>, val context:
                     } else {
                         //通话
                         val tcpModel = VoiceUtil.voiceCall(Constants.deviceId, itemData.deviceId)
-                        val intent = Intent(context, CallSingleActivity::class.java)
+                        val intent: Intent
+                        if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                            intent = Intent(context, SipCallActivity::class.java)
+                        } else {
+                            intent = Intent(context, CallSingleActivity::class.java)
+                        }
                         intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, Constants.sipId)
                         intent.putExtra(CallSingleActivity.EXTRA_MO, true)
                         intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)

+ 22 - 25
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/service/FloatingService.java

@@ -19,11 +19,15 @@ import androidx.annotation.Nullable;
 
 import com.google.gson.Gson;
 import com.wdkl.ncs.android.component.home.R;
+import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig;
+import com.wdkl.ncs.android.component.home.ui.BaseCallActivity;
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity;
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity;
 import com.wdkl.ncs.android.component.home.util.ActivityStackUtil;
 import com.wdkl.ncs.android.component.home.util.MediaPlayHelper;
 import com.wdkl.ncs.android.component.home.util.RingPlayHelper;
 import com.wdkl.ncs.android.component.home.util.SpeechUtil;
+import com.wdkl.ncs.android.lib.base.BaseApplication;
 import com.wdkl.ncs.android.middleware.common.Constants;
 import com.wdkl.ncs.android.lib.vo.MessageEvent;
 import com.wdkl.ncs.android.middleware.model.vo.InteractionVO;
@@ -152,7 +156,12 @@ public class FloatingService extends Service {
 
     private void resumeCallActivity() {
         callResumed = true;
-        Intent intent = new Intent(FloatingService.this, CallSingleActivity.class);
+        Intent intent;
+        if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+            intent = new Intent(FloatingService.this, SipCallActivity.class);
+        } else {
+            intent = new Intent(FloatingService.this, CallSingleActivity.class);
+        }
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         startActivity(intent);
         Toast.makeText(getApplicationContext(), "Resume", Toast.LENGTH_LONG).show();
@@ -202,6 +211,12 @@ public class FloatingService extends Service {
         }
     }
 
+    private void finishCallActivity() {
+        BaseCallActivity callSingleActivity = ActivityStackUtil.getCallSingleActivity();
+        if (callSingleActivity != null) {
+            callSingleActivity.finish();
+        }
+    }
 
     @Subscribe(threadMode = ThreadMode.MAIN)
     public void onEvent(MessageEvent messageEvent) {
@@ -226,49 +241,31 @@ public class FloatingService extends Service {
                 }
             } else if (tcpModel.getAction() == TcpAction.VoiceAction.HANDOFF) { //对方挂断
                 if (curInteractionId == Constants.Companion.getInteractionId()) {
-                    CallSingleActivity callSingleActivity = ActivityStackUtil.getCallSingleActivity();
-                    if (callSingleActivity != null) {
-                        callSingleActivity.finish();
-                    }
+                    finishCallActivity();
                 }
             } else if (tcpModel.getAction() == TcpAction.VoiceAction.REJECT) {
                 if (curInteractionId == Constants.Companion.getInteractionId()) {
-                    CallSingleActivity callSingleActivity = ActivityStackUtil.getCallSingleActivity();
-                    if (callSingleActivity != null) {
-                        callSingleActivity.finish();
-                    }
+                    finishCallActivity();
                 }
             } else if (tcpModel.getAction() == TcpAction.VoiceAction.CALLING) {
                 if (curInteractionId == Constants.Companion.getInteractionId()) {
-                    CallSingleActivity callSingleActivity = ActivityStackUtil.getCallSingleActivity();
-                    if (callSingleActivity != null) {
-                        callSingleActivity.finish();
-                    }
+                    finishCallActivity();
                 }
             } else if (tcpModel.getAction() == TcpAction.VoiceAction.FAILED) {
                 if (curInteractionId == Constants.Companion.getInteractionId()) {
-                    CallSingleActivity callSingleActivity = ActivityStackUtil.getCallSingleActivity();
-                    if (callSingleActivity != null) {
-                        callSingleActivity.finish();
-                    }
+                    finishCallActivity();
                 }
             } else if (tcpModel.getAction() == TcpAction.VoiceAction.CANCEL || tcpModel.getAction() == TcpAction.VoiceAction.VOICE_OFF) {
                 if (curInteractionId == Constants.Companion.getInteractionId()) {
                     RingPlayHelper.stopRingTone();
                     SpeechUtil.getInstance().stopSpeak();
-                    CallSingleActivity callSingleActivity = ActivityStackUtil.getCallSingleActivity();
-                    if (callSingleActivity != null) {
-                        callSingleActivity.finish();
-                    }
+                    finishCallActivity();
                 }
             }
         } else if (tag == 1) {
             TcpModel tcpModel = (TcpModel) messageEvent.getMessage();
             if (tcpModel.getAction() == TcpAction.VoiceAction.SUCCESS){ //服务器返回成功
-                CallSingleActivity callSingleActivity = ActivityStackUtil.getCallSingleActivity();
-                if (callSingleActivity != null) {
-                    callSingleActivity.callOutSuccess(tcpModel);
-                }
+                finishCallActivity();
 
                 //重新拉起通话界面
                 resumeCallActivity();

+ 13 - 2
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/service/WdKeepAliveService.kt

@@ -25,6 +25,7 @@ import com.wdkl.ncs.android.component.home.activity.NewEventListActivity
 import com.wdkl.ncs.android.component.home.helper.HttpHelper
 import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig
 import com.wdkl.ncs.android.component.home.ui.CallSingleActivity
+import com.wdkl.ncs.android.component.home.ui.SipCallActivity
 import com.wdkl.ncs.android.component.home.util.*
 import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.lib.utils.AppTool
@@ -295,7 +296,12 @@ class WdKeepAliveService : AbsWorkService() {
                         SpeechUtil.getInstance().stopSpeak()
 
                         //启动 activity
-                        val intent = Intent(this, CallSingleActivity::class.java)
+                        val intent: Intent
+                        if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                            intent = Intent(this, SipCallActivity::class.java)
+                        } else {
+                            intent = Intent(this, CallSingleActivity::class.java)
+                        }
                         intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, roomId)
                         intent.putExtra(CallSingleActivity.EXTRA_MO, false)
                         intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)
@@ -335,7 +341,12 @@ class WdKeepAliveService : AbsWorkService() {
                     Log.i(TAG, "来电:" + JSON.toJSONString(interactionVO))
 
                     //启动 activity
-                    val intent = Intent(this, CallSingleActivity::class.java)
+                    val intent: Intent
+                    if (SettingConfig.getSipEnabled(BaseApplication.appContext)) {
+                        intent = Intent(this, SipCallActivity::class.java)
+                    } else {
+                        intent = Intent(this, CallSingleActivity::class.java)
+                    }
                     intent.putExtra(CallSingleActivity.EXTRA_ROOM_ID, roomId)
                     intent.putExtra(CallSingleActivity.EXTRA_MO, false)
                     intent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, true)

+ 35 - 0
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/BaseCallActivity.java

@@ -0,0 +1,35 @@
+package com.wdkl.ncs.android.component.home.ui;
+
+import android.os.Vibrator;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.enation.javashop.utils.base.widget.LoadingDialog;
+import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel;
+import com.wdkl.ncs.host.sip.core.LinphoneManager;
+
+public abstract class BaseCallActivity extends AppCompatActivity {
+
+    public static final String EXTRA_ROOM_ID = "roomId";
+    public static final String EXTRA_MO = "isOutGoing";
+    public static final String EXTRA_AUDIO_ONLY = "audioOnly";
+    public static final String EXTRA_TCPMODEL = "tcpModel";
+    public static final String EXTRA_SHOWNAME = "showName";
+    public static final String EXTRA_RING = "ring";
+
+    TcpModel recTcpModel;
+    String showName;
+    LoadingDialog loadingDialog;
+    boolean ringUp = true;
+    boolean callConnected = false;
+    //振动
+    Vibrator mVibrator;
+
+    LinphoneManager linphoneManager = null;
+
+    abstract public void callOutSuccess(TcpModel tcpModel);
+
+    abstract void sendHandOffTcp();
+
+    abstract boolean isOutgoing();
+}

+ 399 - 0
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/BaseSipCallFragment.java

@@ -0,0 +1,399 @@
+package com.wdkl.ncs.android.component.home.ui;
+
+import android.annotation.SuppressLint;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Chronometer;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.alibaba.fastjson.JSONObject;
+import com.google.gson.Gson;
+import com.wdkl.ncs.android.component.home.R;
+import com.wdkl.ncs.android.component.home.util.MediaPlayHelper;
+import com.wdkl.ncs.android.component.home.util.RingPlayHelper;
+import com.wdkl.ncs.android.component.home.util.SpeechUtil;
+import com.wdkl.ncs.android.lib.base.BaseApplication;
+import com.wdkl.ncs.android.lib.utils.ExtendMethodsKt;
+import com.wdkl.ncs.android.lib.vo.MessageEvent;
+import com.wdkl.ncs.android.middleware.common.Constants;
+import com.wdkl.ncs.android.middleware.model.vo.InteractionVO;
+import com.wdkl.ncs.android.middleware.tcp.TcpClient;
+import com.wdkl.ncs.android.middleware.tcp.dto.TcpCallback;
+import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel;
+import com.wdkl.ncs.android.middleware.tcp.enums.TcpAction;
+import com.wdkl.ncs.host.sip.core.LinphoneManager;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public abstract class BaseSipCallFragment extends Fragment {
+    private static final String TAG = "BaseSipCallFragment";
+    ImageView minimizeImageView;
+    ImageView portraitImageView;// 用户头像
+    TextView nameTextView; // 用户昵称
+    TextView descTextView;  // 状态提示用语
+    Chronometer durationTextView; // 通话时长
+
+    LinearLayout transLinearLayout;
+    LinearLayout hangupLinearLayout;
+    LinearLayout muteLinearLayout;
+    LinearLayout speakerLinearLayout;
+
+    ImageView outgoingHangupImageView;
+    ImageView incomingHangupImageView;
+    ImageView incomingTransImageView;
+    ImageView acceptImageView;
+    TextView tvStatus;
+    View outgoingActionContainer;
+    View incomingActionContainer;
+    View connectedActionContainer;
+
+    View lytParent;
+
+    boolean isOutgoing = true;
+
+    BaseCallActivity callActivity;
+    CallHandler handler;
+    boolean isHandoff = false;
+
+    public static final int WHAT_DELAY_END_CALL = 0x01;
+    public static final int FINISH_CURRENT = 0x02;
+    public static final int WHAT_DELAY_CHECK_CALL = 0x03;
+
+    HeadsetPlugReceiver headsetPlugReceiver;
+
+    private boolean callSuccess = false;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRetainInstance(true);
+
+        handler = new CallHandler();
+        EventBus.getDefault().register(this);
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(getLayout(), container, false);
+        initView(view);
+        init();
+        return view;
+    }
+
+    @Override
+    public void onViewCreated(View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+
+        if (isOutgoing) {
+            RingPlayHelper.playRingTone(BaseApplication.appContext, R.raw.ring_back2, true);
+        } else {
+            if (callActivity.ringUp) {
+                RingPlayHelper.playRingTone(BaseApplication.appContext, R.raw.ring_tone, true);
+            }
+        }
+    }
+
+    @Override
+    public void onDestroyView() {
+        if (handler != null) {
+            handler.removeCallbacksAndMessages(null);
+        }
+        if (durationTextView != null)
+            durationTextView.stop();
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        EventBus.getDefault().unregister(this);
+    }
+
+    abstract int getLayout();
+    abstract void showCallConnected();
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        callActivity = (SipCallActivity) getActivity();
+        if (callActivity != null) {
+            isOutgoing = callActivity.isOutgoing();
+            headsetPlugReceiver = new HeadsetPlugReceiver();
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_HEADSET_PLUG);
+            callActivity.registerReceiver(headsetPlugReceiver, filter);
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        callActivity.unregisterReceiver(headsetPlugReceiver);  //注销监听
+        callActivity = null;
+    }
+
+    public void initView(View view) {
+        lytParent = view.findViewById(R.id.lytParent);
+        minimizeImageView = view.findViewById(R.id.minimizeImageView);
+        portraitImageView = view.findViewById(R.id.portraitImageView);
+        nameTextView = view.findViewById(R.id.nameTextView);
+        descTextView = view.findViewById(R.id.descTextView);
+        durationTextView = view.findViewById(R.id.durationTextView);
+        outgoingHangupImageView = view.findViewById(R.id.outgoingHangupImageView);
+        incomingHangupImageView = view.findViewById(R.id.incomingHangupImageView);
+        incomingTransImageView = view.findViewById(R.id.incomingTransImageView);
+        acceptImageView = view.findViewById(R.id.acceptImageView);
+        tvStatus = view.findViewById(R.id.tvStatus);
+        outgoingActionContainer = view.findViewById(R.id.outgoingActionContainer);
+        incomingActionContainer = view.findViewById(R.id.incomingActionContainer);
+        connectedActionContainer = view.findViewById(R.id.connectedActionContainer);
+        transLinearLayout = view.findViewById(R.id.transLinearLayout);
+        hangupLinearLayout = view.findViewById(R.id.hangupLinearLayout);
+        muteLinearLayout = view.findViewById(R.id.muteLinearLayout);
+        speakerLinearLayout = view.findViewById(R.id.speakerLinearLayout);
+
+        durationTextView.setVisibility(View.GONE);
+        if (isOutgoing) {
+            handler.sendEmptyMessageDelayed(WHAT_DELAY_CHECK_CALL, 5000); //5s后检查是否呼叫成功
+        }
+        //handler.sendEmptyMessageDelayed(WHAT_DELAY_END_CALL, 60 * 1000);//60s之后未接通,则挂断电话
+    }
+
+    public void init() {
+        if (callActivity.recTcpModel.getData()!=null){
+            InteractionVO interactionVO = new Gson().fromJson(callActivity.recTcpModel.getData().toString(), InteractionVO.class);
+            Constants.Companion.setInteractionId(interactionVO.getId());
+        }
+
+        if (isOutgoing) {
+            startOutCall();
+        }
+    }
+
+    @SuppressLint("CheckResult")
+    private void startOutCall() {
+        //发送tcp call
+        TcpCallback transaction = new TcpCallback(callActivity.recTcpModel.getTid()) {
+            @Override
+            public void onSuccess(JSONObject jsonObject) {
+                //
+            }
+
+            @Override
+            public void onFailed(JSONObject jsonObject) {
+                if (callActivity != null) {
+                    callActivity.runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            ExtendMethodsKt.showMessage("Failed");
+                        }
+                    });
+                    callActivity.finish();
+                }
+            }
+        };
+        TcpClient.getInstance().sendTcp(callActivity.recTcpModel, false, transaction);
+    }
+
+    public void callOutSuccess(TcpModel tcpModel) {
+        callActivity.recTcpModel = tcpModel;
+        InteractionVO interactionVO = new Gson().fromJson(tcpModel.getData().toString(), InteractionVO.class);
+        Constants.Companion.setInteractionId(interactionVO.getId());
+        callSuccess = true;
+        //callSingleActivity.janusClient.connect();
+        Log.d(TAG,"voice#success =>" + callActivity.recTcpModel.toJson());
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                Toast.makeText(BaseSipCallFragment.this.getContext(), R.string.call_success, Toast.LENGTH_SHORT).show();
+            }
+        });
+    }
+
+    public void startRefreshTime() {
+        if (durationTextView != null) {
+            durationTextView.setOnChronometerTickListener(null);
+            durationTextView.stop();
+            durationTextView.setVisibility(View.VISIBLE);
+            durationTextView.setBase(SystemClock.elapsedRealtime());
+            durationTextView.start();
+        }
+    }
+
+    void runOnUiThread(Runnable runnable) {
+        if (callActivity != null) {
+            callActivity.runOnUiThread(runnable);
+        }
+    }
+
+    @SuppressLint("HandlerLeak")
+    class CallHandler extends Handler {
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            switch (msg.what) {
+                case WHAT_DELAY_END_CALL:
+                    if (callActivity != null && !isHandoff && !callActivity.linphoneManager.isCallConnected()) {
+                        callActivity.sendHandOffTcp();
+                        callActivity.finish();
+                    }
+                    break;
+                case WHAT_DELAY_CHECK_CALL:
+                    if (callActivity != null && !callSuccess) {
+                        Toast.makeText(BaseSipCallFragment.this.getContext(), R.string.net_error, Toast.LENGTH_SHORT).show();
+                    }
+                    break;
+                case FINISH_CURRENT:
+                    if (callActivity != null) {
+                        callActivity.finish();
+                    }
+                    break;
+            }
+        }
+    }
+
+    public abstract void onBackPressed();
+
+    public abstract boolean onKeyUp(int keyCode, KeyEvent event);
+
+    protected abstract void handleHeadsetHook();
+
+    class HeadsetPlugReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.hasExtra("state")) {
+                if (intent.getIntExtra("state", 0) == 0) { //拔出耳机
+                    callActivity.linphoneManager.enableSpeaker(true);
+                } else if (intent.getIntExtra("state", 0) == 1) { //插入耳机
+                    callActivity.linphoneManager.routeToHeadset();
+                }
+            }
+        }
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    public void onEvent(MessageEvent messageEvent) {
+        int code = messageEvent.getTag();
+        //TCP处理
+        if (code == 2) {
+            TcpModel tcpModel = (TcpModel) messageEvent.getMessage();
+            Log.i(TAG, "收到数据222: " + tcpModel.toJson());
+            int curInteractionId = -1;
+            InteractionVO interactionVO = null;
+            if (tcpModel.getData() != null) {
+                interactionVO = new Gson().fromJson(tcpModel.getData().toString(), InteractionVO.class);
+                curInteractionId = interactionVO.getId();
+            }
+
+            if (tcpModel.getAction() == TcpAction.VoiceAction.ACCEPT) {
+                if (curInteractionId == Constants.Companion.getInteractionId()) {
+                    MediaPlayHelper.getInstance().stopMusic(true);
+                    RingPlayHelper.stopRingTone();
+                    SpeechUtil.getInstance().stopSpeak();
+                    callActivity.recTcpModel = tcpModel;
+                    Log.d(TAG, "voice#accept =>" + callActivity.recTcpModel.toJson());
+                    Log.i(TAG, "对方接听电话啦");
+
+                    if (interactionVO == null || TextUtils.isEmpty(interactionVO.getToSipId())) {
+                        //通话失败,重置并返回主界面
+                        Toast.makeText(BaseSipCallFragment.this.getContext(), "targetSipId empty!", Toast.LENGTH_SHORT).show();
+                        handler.sendEmptyMessageDelayed(FINISH_CURRENT, 1000);
+                    } else {
+                        callActivity.linphoneManager.startCall(interactionVO.getToSipId(), false);
+                    }
+                }
+            } else if (tcpModel.getAction() == TcpAction.VoiceAction.HANDOFF) { //对方挂断
+                if (curInteractionId == Constants.Companion.getInteractionId()) {
+                    isHandoff = true;
+                    runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            Toast.makeText(BaseSipCallFragment.this.getContext(), R.string.call_end, Toast.LENGTH_SHORT).show();
+                        }
+                    });
+
+                    handler.sendEmptyMessageDelayed(FINISH_CURRENT, 100);
+                }
+            } else if (tcpModel.getAction() == TcpAction.VoiceAction.REJECT) {
+                if (curInteractionId == Constants.Companion.getInteractionId()) {
+                    handler.sendEmptyMessageDelayed(FINISH_CURRENT, 100);
+
+                    runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            Toast.makeText(BaseSipCallFragment.this.getContext(), R.string.call_reject, Toast.LENGTH_SHORT).show();
+                        }
+                    });
+                }
+            } else if (tcpModel.getAction() == TcpAction.VoiceAction.CALLING) {
+                if (curInteractionId == Constants.Companion.getInteractionId()) {
+                    handler.sendEmptyMessageDelayed(FINISH_CURRENT, 100);
+                    runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            Toast.makeText(BaseSipCallFragment.this.getContext(), R.string.call_busy, Toast.LENGTH_SHORT).show();
+                        }
+                    });
+                }
+            } else if (tcpModel.getAction() == TcpAction.VoiceAction.FAILED) {
+                //if (curInteractionId == interactionId) {
+                    handler.sendEmptyMessageDelayed(FINISH_CURRENT, 1000);
+                    runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            Toast.makeText(BaseSipCallFragment.this.getContext(), R.string.call_failed, Toast.LENGTH_SHORT).show();
+                        }
+                    });
+                //}
+            } else if (tcpModel.getAction() == TcpAction.VoiceAction.CANCEL || tcpModel.getAction() == TcpAction.VoiceAction.VOICE_OFF) {
+                if (curInteractionId == Constants.Companion.getInteractionId()) {
+                    handler.sendEmptyMessageDelayed(FINISH_CURRENT, 100);
+                    runOnUiThread(new Runnable() {
+                        @Override
+                        public void run() {
+                            Toast.makeText(BaseSipCallFragment.this.getContext(), R.string.str_cancel, Toast.LENGTH_SHORT).show();
+                        }
+                    });
+                }
+            }
+        } else if (code == 1) {
+            TcpModel tcpModel = (TcpModel) messageEvent.getMessage();
+            Log.i(TAG, "收到数据111: " + tcpModel.toJson());
+            if (isOutgoing && tcpModel.getAction() == TcpAction.VoiceAction.SUCCESS){ //服务器返回成功
+                callOutSuccess(tcpModel);
+            }
+        } else if (code == Constants.EVENT_HEADSET_HOOK) {
+            //收到耳机按键
+            handleHeadsetHook();
+        } else if (code == Constants.EVENT_END_CALL) {
+            handler.sendEmptyMessageDelayed(FINISH_CURRENT, 100);
+        } else if (code == Constants.SIP_CONNECTED) {
+            callActivity.linphoneManager.enableMic(true);
+            showCallConnected();
+        }
+    }
+}

+ 2 - 21
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/CallSingleActivity.java

@@ -12,7 +12,6 @@ import android.graphics.Color;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.IBinder;
-import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.provider.Settings;
 import android.util.Log;
@@ -22,12 +21,9 @@ import android.view.Window;
 import android.view.WindowManager;
 import android.widget.Toast;
 
-import androidx.appcompat.app.AppCompatActivity;
 import androidx.fragment.app.FragmentManager;
 
-import com.alibaba.fastjson.JSONObject;
 import com.enation.javashop.utils.base.tool.CommonTool;
-import com.enation.javashop.utils.base.widget.LoadingDialog;
 import com.google.gson.Gson;
 import com.wdkl.ncs.android.component.home.R;
 import com.wdkl.ncs.android.component.home.service.FloatingService;
@@ -45,7 +41,6 @@ import com.wdkl.ncs.android.middleware.model.vo.InteractionVO;
 import com.wdkl.ncs.android.middleware.tcp.TcpClient;
 import com.wdkl.ncs.android.middleware.tcp.channel.DeviceChannel;
 import com.wdkl.ncs.android.middleware.tcp.channel.VoiceUtil;
-import com.wdkl.ncs.android.middleware.tcp.dto.TcpCallback;
 import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel;
 import com.wdkl.ncs.android.middleware.tcp.enums.DeviceTypeEnum;
 import com.wdkl.ncs.android.middleware.tcp.enums.TcpAction;
@@ -65,15 +60,7 @@ import pub.devrel.easypermissions.EasyPermissions;
 /**
  * 单人通话界面
  */
-public class CallSingleActivity extends AppCompatActivity {
-
-    public static final String EXTRA_ROOM_ID = "roomId";
-    public static final String EXTRA_MO = "isOutGoing";
-    public static final String EXTRA_AUDIO_ONLY = "audioOnly";
-    public static final String EXTRA_TCPMODEL = "tcpModel";
-    public static final String EXTRA_SHOWNAME = "showName";
-    public static final String EXTRA_RING = "ring";
-
+public class CallSingleActivity extends BaseCallActivity {
     private static final String TAG = "CallSingleActivity";
 
     private boolean isOutgoing;
@@ -82,19 +69,12 @@ public class CallSingleActivity extends AppCompatActivity {
     private BigInteger localUserId = BigInteger.valueOf(Constants.Companion.getMemberId());
 
     boolean isAudioOnly = true;
-    String showName;
-    TcpModel recTcpModel;
     JanusClient janusClient;
     ILogUpload iLogUpload = new LogUpload();
     VideoRoomCallback videoRoomCallback;
     boolean isSimulateBed = false;
-    boolean ringUp = true;
-    boolean callConnected = false;
 
     private SingleCallFragment currentFragment;
-    //振动
-    Vibrator mVibrator;
-    public LoadingDialog loadingDialog;
 
     private boolean mServiceBound = false;
     private HomeWatcher mHomeWatcher;
@@ -292,6 +272,7 @@ public class CallSingleActivity extends AppCompatActivity {
         // 7,在 videoRoomCallback.onSubscribeAttached 中订阅发布者,如果有 publisher 的话
     }
 
+    @Override
     public void callOutSuccess(TcpModel tcpModel) {
         if (currentFragment != null) {
             currentFragment.callOutSuccess(tcpModel);

+ 0 - 46
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/FragmentAudio.java

@@ -314,44 +314,8 @@ public class FragmentAudio extends SingleCallFragment implements View.OnClickLis
         }
 
         TcpModel voiceTransferTcpModel = VoiceUtil.voiceTransfer(callSingleActivity.recTcpModel.getTid(), Constants.Companion.getDeviceId(), toId, interactionVO.getId());
-        /*TcpCallback transaction = new TcpCallback(voiceTransferTcpModel.getTid()) {
-            @Override
-            public void onSuccess(JSONObject jsonObject) {
-                if (callSingleActivity != null) {
-                    callSingleActivity.runOnUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            ExtendMethodsKt.showMessage("转接成功");
-                        }
-                    });
-                }
-                handler.sendEmptyMessageDelayed(FINISH_CURRENT, 500);
-            }
-
-            @Override
-            public void onFailed(JSONObject jsonObject) {
-                if (callSingleActivity != null) {
-                    callSingleActivity.runOnUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            ExtendMethodsKt.showMessage("转接失败");
-                        }
-                    });
-                }
-            }
-        };*/
         TcpClient.getInstance().sendTcp(voiceTransferTcpModel, false, null);
         handler.sendEmptyMessageDelayed(FINISH_CURRENT, 500);
-
-        /*NettyClient.Companion.getInstance().sendMsg(voiceTransferTcpModel.toJson()).subscribe(it-> {
-            if (it) {
-                Log.d(TAG, "TCP.发送消息完成");
-                handler.sendEmptyMessageDelayed(FINISH_CURRENT, 500);
-            } else {
-                Log.e(TAG, "TCP.发送消息失败");
-                HandleTcpConnect.Companion.getInstance().tcpReConnectWithMsgShow();
-            }
-        });*/
     }
 
     public void acceptCall(){
@@ -398,16 +362,6 @@ public class FragmentAudio extends SingleCallFragment implements View.OnClickLis
             };
             TcpClient.getInstance().sendTcp(voiceUtilTcpModel, false, transaction);
 
-            /*NettyClient.Companion.getInstance().sendMsg(voiceUtilTcpModel.toJson()).subscribe(it->{
-                if (it) {
-                    Log.d(TAG, "TCP.发送消息完成");
-                } else {
-                    Log.e(TAG, "TCP.发送消息失败");
-                    callSingleActivity.loadingDialog.dismiss();
-                    HandleTcpConnect.Companion.getInstance().tcpReConnectWithMsgShow();
-                }
-            });*/
-
             transHandler.sendEmptyMessageDelayed(CHECK, 6000);
         }
     }

+ 422 - 0
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/FragmentSipAudio.java

@@ -0,0 +1,422 @@
+package com.wdkl.ncs.android.component.home.ui;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.alibaba.fastjson.JSONObject;
+import com.blankj.utilcode.util.BarUtils;
+import com.enation.javashop.utils.base.tool.CommonTool;
+import com.google.gson.Gson;
+import com.wdkl.ncs.android.component.home.R;
+import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig;
+import com.wdkl.ncs.android.component.home.util.LocaleMangerUtils;
+import com.wdkl.ncs.android.component.home.util.MediaPlayHelper;
+import com.wdkl.ncs.android.component.home.util.RingPlayHelper;
+import com.wdkl.ncs.android.component.home.util.SpeechUtil;
+import com.wdkl.ncs.android.component.home.util.Util;
+import com.wdkl.ncs.android.component.home.util.VoiceManagerUtil;
+import com.wdkl.ncs.android.lib.utils.ExtendMethodsKt;
+import com.wdkl.ncs.android.middleware.common.Constants;
+import com.wdkl.ncs.android.middleware.model.vo.InteractionVO;
+import com.wdkl.ncs.android.middleware.tcp.TcpClient;
+import com.wdkl.ncs.android.middleware.tcp.channel.VoiceUtil;
+import com.wdkl.ncs.android.middleware.tcp.dto.TcpCallback;
+import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel;
+import com.wdkl.ncs.android.middleware.tcp.enums.DeviceTypeEnum;
+import com.wdkl.ncs.android.middleware.utils.StringUtil;
+
+import java.util.Locale;
+
+/**
+ * 语音通话控制界面
+ */
+public class FragmentSipAudio extends BaseSipCallFragment implements View.OnClickListener {
+    private static final String TAG = "FragmentSipAudio";
+    private ImageView muteImageView;
+    private ImageView speakerImageView;
+    private boolean micEnabled = false; // 静音
+    private boolean isSpeakerOn = false; // 扬声器
+    private boolean accepted = false;
+
+    TransHandler transHandler;
+
+    @Override
+    int getLayout() {
+        return R.layout.fragment_audio;
+    }
+
+    @Override
+    public void initView(View view) {
+        super.initView(view);
+        muteImageView = view.findViewById(R.id.muteImageView);
+        speakerImageView = view.findViewById(R.id.speakerImageView);
+
+        minimizeImageView.setVisibility(View.GONE);
+        outgoingHangupImageView.setOnClickListener(this);
+        incomingHangupImageView.setOnClickListener(this);
+        incomingTransImageView.setOnClickListener(this);
+        minimizeImageView.setOnClickListener(this);
+        muteImageView.setOnClickListener(this);
+        acceptImageView.setOnClickListener(this);
+        speakerImageView.setOnClickListener(this);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            lytParent.post(() -> {
+                RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) minimizeImageView.getLayoutParams();
+                params.topMargin = BarUtils.getStatusBarHeight();
+                minimizeImageView.setLayoutParams(params);
+            });
+        }
+    }
+
+    @Override
+    public void init() {
+        super.init();
+
+        if (callActivity.loadingDialog==null){
+            callActivity.loadingDialog = CommonTool.createLoadingDialog(this.getContext(), R.layout.custom_loading,R.id.loadding_image);
+        }
+        InteractionVO interactionVO = null;
+        if (callActivity.recTcpModel.getData()!=null) {
+            interactionVO = new Gson().fromJson(callActivity.recTcpModel.getData().toString(), InteractionVO.class);
+        }
+        transHandler = new TransHandler();
+        //是来电,且是客户来电进入延时转接
+        /*if (!isOutgoing && interactionVO.getFromClerkId()==null) {
+            int transSeconds = SettingConfig.getCountdownTime(this.getContext());
+            transHandler.sendEmptyMessageDelayed(TRANS, transSeconds * 1000);
+        }*/
+
+        // 如果已经接通
+        if (callActivity.linphoneManager.isCallConnected()) {
+            descTextView.setVisibility(View.GONE); // 提示语
+            outgoingActionContainer.setVisibility(View.VISIBLE);
+            durationTextView.setVisibility(View.VISIBLE);
+            startRefreshTime();
+        } else {
+            // 如果未接通
+            if (isOutgoing) {
+                descTextView.setText(getShowName() + "\n" + StringUtil.getResString(R.string.call_in_calling));
+                outgoingActionContainer.setVisibility(View.VISIBLE);
+                incomingActionContainer.setVisibility(View.GONE);
+                //MediaPlayHelper.getInstance().playResMusic(R.raw.outgoing_call, 0.8f, true);
+            } else {
+                descTextView.setText(getShowName() + "\n" + StringUtil.getResString(R.string.call_incoming));
+                outgoingActionContainer.setVisibility(View.GONE);
+                incomingActionContainer.setVisibility(View.VISIBLE);
+
+                //tts未初始化或者初始化不成功时重新初始化
+                if (SpeechUtil.getInstance().getTextToSpeech() == null || Constants.Companion.getTtsState() != 2) {
+                    SpeechUtil.getInstance().reInitSpeech();
+                }
+
+                if (interactionVO != null) {
+                    String language = LocaleMangerUtils.getApplicationLocale().getLanguage();
+                    String frameName;
+                    if (Locale.CHINESE.getLanguage().equals(language)) {
+                        frameName = Util.INSTANCE.appendSpace(getShowName());
+                    } else {
+                        frameName = getShowName();
+                    }
+
+                    /*String text = frameName + getString(R.string.call_incoming) + frameName + getString(R.string.call_incoming)
+                            + frameName + getString(R.string.call_incoming) + frameName + getString(R.string.call_incoming);
+                    SpeechUtil.getInstance().startSpeak(text);*/
+                }
+            }
+        }
+    }
+
+    private String getShowName(){
+        InteractionVO interactionVO = null;
+        if (callActivity.recTcpModel.getData()!=null) {
+            interactionVO = new Gson().fromJson(callActivity.recTcpModel.getData().toString(), InteractionVO.class);
+        }
+
+        String showName = "";
+        if (interactionVO==null){
+            showName = callActivity.showName;
+        } else {
+            //是自己拨出
+            if (interactionVO.getFromDeviceId().equals(Constants.Companion.getDeviceId())) {
+                if (interactionVO.getToClerkId() == null) {
+                    showName = interactionVO.getToFrameFullName() + " " + interactionVO.getToMemberName();
+                    portraitImageView.setImageResource(R.drawable.face_customer);
+                } else {
+                    showName = interactionVO.getToMemberName();
+                    portraitImageView.setImageResource(R.drawable.face_clerk);
+                }
+            }
+            //是他人呼入
+            else {
+                //是客户
+                if (interactionVO.getFromClerkId() == null) {
+                    //showName = interactionVO.getFromFrameFullName() /*+ " " + interactionVO.getFromMemberName()*/;
+                    //portraitImageView.setImageResource(R.drawable.face_customer);
+                    showName = interactionVO.getFromFrameFullName().replace("-", " ");
+
+                    if (!isOutgoing) {
+                        /*if (Constants.Companion.getRoleId() != null) {
+                            if (interactionVO.getFromDeviceType() == DeviceTypeEnum.DIGIT_BED_DEVICE.value()
+                            || interactionVO.getFromDeviceType() == DeviceTypeEnum.SIMULATE_BED_DEVICE.value()) {
+                                //客户分机呼叫只能接听或转接,禁用拒接
+                                transLinearLayout.setVisibility(View.VISIBLE);
+                            } else {
+                                hangupLinearLayout.setVisibility(View.VISIBLE);
+                            }
+                        }*/
+
+                        hangupLinearLayout.setVisibility(View.VISIBLE);
+                    }
+                }
+                //是同事
+                else {
+                    showName = interactionVO.getFromMemberName();
+                    portraitImageView.setImageResource(R.drawable.face_clerk);
+
+                    if (!isOutgoing) {
+                        hangupLinearLayout.setVisibility(View.VISIBLE);
+                    }
+                }
+            }
+        }
+        return showName;
+    }
+
+    @Override
+    public void onBackPressed() {
+        super.onDestroy();
+    }
+
+    @Override
+    public void onDestroyView() {
+        transHandler.removeCallbacksAndMessages(null);
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        if (callActivity != null && callActivity.linphoneManager.isCallConnected()) {
+            //通话已连接上
+            showCallConnected();
+        }
+    }
+
+    @Override
+    void showCallConnected() {
+        Log.e(TAG, "show call connected");
+        if (callActivity.callConnected) {
+            return;
+        }
+        callActivity.callConnected = true;
+
+        MediaPlayHelper.getInstance().stopMusic(true);
+        RingPlayHelper.stopRingTone();
+        SpeechUtil.getInstance().stopSpeak();
+
+        runOnUiThread(() -> {
+            //callActivity.loadingDialog.dismiss();
+
+            incomingActionContainer.setVisibility(View.GONE);
+            outgoingActionContainer.setVisibility(View.VISIBLE);
+            //minimizeImageView.setVisibility(View.VISIBLE);
+            descTextView.setVisibility(View.GONE);
+
+            muteLinearLayout.setVisibility(View.VISIBLE);
+            speakerLinearLayout.setVisibility(View.VISIBLE);
+
+            isSpeakerOn = !isSpeakerOn;
+            if (VoiceManagerUtil.isHeadphonesPlugged(callActivity)) {
+                Log.d(TAG, "didChangeState 耳机" + isSpeakerOn);
+                callActivity.linphoneManager.routeToHeadset();
+            } else {
+                Log.d(TAG, "didChangeState 外放切换" + isSpeakerOn);
+                callActivity.linphoneManager.enableSpeaker(isSpeakerOn);
+                speakerImageView.setSelected(isSpeakerOn);
+            }
+
+            nameTextView.setText(getShowName());
+
+            startRefreshTime();
+        });
+    }
+
+    public static final int TRANS = 0x03;
+    public static final int CHECK = 0x04;
+
+    class TransHandler extends Handler {
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            if (msg.what == TRANS) {
+                if (callActivity != null && !isHandoff && !callActivity.linphoneManager.isCallConnected()) {
+                    transCall();
+                }
+            } else if (msg.what == CHECK) {
+                if (callActivity != null && !isHandoff && !callActivity.linphoneManager.isCallConnected()) {
+                    Toast.makeText(callActivity, R.string.call_disconnect, Toast.LENGTH_LONG).show();
+                }
+            }
+        }
+    }
+
+    @SuppressLint("CheckResult")
+    private void transCall(){
+        durationTextView.setText(R.string.str_call_transfer);
+        //callActivity.loadingDialog.show();
+        MediaPlayHelper.getInstance().stopMusic(true);
+        RingPlayHelper.stopRingTone();
+        SpeechUtil.getInstance().stopSpeak();
+
+        //给服务器发送转接 tcp
+        InteractionVO interactionVO = new Gson().fromJson(callActivity.recTcpModel.getData().toString(), InteractionVO.class);
+        int toId;
+        if (interactionVO.getFromDeviceId().equals(Constants.Companion.getDeviceId())) {
+            toId = interactionVO.getToDeviceId();
+        } else {
+            toId = interactionVO.getFromDeviceId();
+        }
+
+        TcpModel voiceTransferTcpModel = VoiceUtil.voiceTransfer(callActivity.recTcpModel.getTid(), Constants.Companion.getDeviceId(), toId, interactionVO.getId());
+        TcpClient.getInstance().sendTcp(voiceTransferTcpModel, false, null);
+        handler.sendEmptyMessageDelayed(FINISH_CURRENT, 500);
+    }
+
+    public void acceptCall(){
+        if (callActivity!=null){
+            //取消自动转接
+            transHandler.removeMessages(TRANS);
+            accepted = true;
+
+            durationTextView.setText(R.string.call_connecting);
+            //callActivity.loadingDialog.show();
+            callActivity.mVibrator.cancel();
+            MediaPlayHelper.getInstance().stopMusic(true);
+            RingPlayHelper.stopRingTone();
+            SpeechUtil.getInstance().stopSpeak();
+            //发出accept
+            InteractionVO interactionVO = new Gson().fromJson(callActivity.recTcpModel.getData().toString(), InteractionVO.class);
+            int toId;
+            if (interactionVO.getFromDeviceId().equals(Constants.Companion.getDeviceId())) {
+                toId = interactionVO.getToDeviceId();
+            } else {
+                toId = interactionVO.getFromDeviceId();
+            }
+
+            TcpModel voiceUtilTcpModel = VoiceUtil.voiceAccept(callActivity.recTcpModel.getTid(), Constants.Companion.getDeviceId(), toId, interactionVO.getId());
+            TcpCallback transaction = new TcpCallback(voiceUtilTcpModel.getTid()) {
+                @Override
+                public void onSuccess(JSONObject jsonObject) {
+                    //
+                }
+
+                @Override
+                public void onFailed(JSONObject jsonObject) {
+                    if (callActivity != null) {
+                        callActivity.runOnUiThread(new Runnable() {
+                            @Override
+                            public void run() {
+                                ExtendMethodsKt.showMessage("Accept failed");
+                                //callSingleActivity.loadingDialog.dismiss();
+                            }
+                        });
+                    }
+                }
+            };
+            
+            TcpClient.getInstance().sendTcp(voiceUtilTcpModel, false, transaction);
+            transHandler.sendEmptyMessageDelayed(CHECK, 6000);
+        }
+    }
+
+    @Override
+    protected void handleHeadsetHook() {
+        //只有来电的时候才接听
+        if (!isOutgoing) {
+            if (accepted) {
+                if (callActivity != null) {
+                    isHandoff = true;
+                    transHandler.removeMessages(TRANS);
+                    callActivity.sendHandOffTcp();
+                    handler.sendEmptyMessageDelayed(FINISH_CURRENT, 100);
+                }
+            } else {
+                acceptCall();
+            }
+        } else {
+            //去电时挂断
+            if (callActivity != null) {
+                isHandoff = true;
+                transHandler.removeMessages(TRANS);
+                callActivity.sendHandOffTcp();
+                handler.sendEmptyMessageDelayed(FINISH_CURRENT, 100);
+            }
+        }
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        Log.i(TAG, "keyup keyCode " + keyCode);
+        if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+            handleHeadsetHook();
+        } else if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
+            return true;
+        }
+        return true;
+    }
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        // 转接
+        if (id == R.id.incomingTransImageView){
+            if (callActivity!=null) {
+                transCall();
+            }
+        }
+        // 接听
+        else if (id == R.id.acceptImageView) {
+            acceptCall();
+        }
+        // 挂断电话
+        else if (id == R.id.incomingHangupImageView || id == R.id.outgoingHangupImageView) {
+            if (callActivity != null) {
+                isHandoff = true;
+                //取消自动转接
+                transHandler.removeMessages(TRANS);
+                callActivity.sendHandOffTcp();
+                handler.sendEmptyMessageDelayed(FINISH_CURRENT, 100);
+            }
+        }
+        // 静音
+        else if (id == R.id.muteImageView) {
+            if (callActivity != null) {
+                micEnabled = callActivity.linphoneManager.micEnabled();
+                Log.d(TAG,"静音切换" + micEnabled);
+                if (micEnabled) {
+                    muteImageView.setSelected(true);
+                    callActivity.linphoneManager.enableMic(false);
+                } else {
+                    muteImageView.setSelected(false);
+                    callActivity.linphoneManager.enableMic(true);
+                }
+            }
+        }
+        // 扬声器
+        else if (id == R.id.speakerImageView) {
+            isSpeakerOn=!isSpeakerOn;
+            Log.d(TAG,"外放切换" + isSpeakerOn);
+            callActivity.linphoneManager.enableSpeaker(isSpeakerOn);
+            speakerImageView.setSelected(isSpeakerOn);
+        }
+    }
+}

+ 1 - 73
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/SingleCallFragment.java

@@ -130,7 +130,7 @@ public abstract class SingleCallFragment extends Fragment {
         }
         if (durationTextView != null)
             durationTextView.stop();
-        refreshMessage(true);
+
         super.onDestroyView();
     }
 
@@ -224,18 +224,6 @@ public abstract class SingleCallFragment extends Fragment {
             }
         };
         TcpClient.getInstance().sendTcp(callSingleActivity.recTcpModel, false, transaction);
-
-        /*NettyClient.Companion.getInstance().sendMsg(callSingleActivity.recTcpModel.toJson()).subscribe(it -> {
-            if (it) {
-                Log.d(TAG, "TCP.发送消息完成");
-            } else {
-                Log.e(TAG, "TCP.发送消息失败");
-                HandleTcpConnect.Companion.getInstance().tcpReConnectWithMsgShow();
-                if (callSingleActivity != null) {
-                    callSingleActivity.finish();
-                }
-            }
-        });*/
     }
 
     public void callOutSuccess(TcpModel tcpModel) {
@@ -253,59 +241,6 @@ public abstract class SingleCallFragment extends Fragment {
         });
     }
 
-    // ======================================界面回调================================
-    /*public void didCallEndWithReason(EnumType.CallEndReason callEndReason) {
-        switch (callEndReason) {
-            case Busy: {
-                tvStatus.setText("对方忙线中");
-                break;
-            }
-            case SignalError: {
-                tvStatus.setText("连接断开");
-                break;
-            }
-            case RemoteSignalError: {
-                tvStatus.setText("对方网络断开");
-                break;
-            }
-            case Hangup: {
-                tvStatus.setText("挂断");
-                break;
-            }
-            case MediaError: {
-                tvStatus.setText("媒体错误");
-                break;
-            }
-            case RemoteHangup: {
-                tvStatus.setText("对方挂断");
-                break;
-            }
-            case OpenCameraFailure: {
-                tvStatus.setText("打开摄像头错误");
-                break;
-            }
-            case Timeout: {
-                tvStatus.setText("对方未接听");
-                break;
-            }
-            case AcceptByOtherClient: {
-                tvStatus.setText("在其它设备接听");
-                break;
-            }
-        }
-        incomingActionContainer.setVisibility(View.GONE);
-        outgoingActionContainer.setVisibility(View.GONE);
-        if (connectedActionContainer != null)
-            connectedActionContainer.setVisibility(View.GONE);
-        refreshMessage(false);
-        new Handler(Looper.getMainLooper()).postDelayed(() -> {
-            if (callSingleActivity != null) {
-                callSingleActivity.finish();
-            }
-
-        }, 11);
-    }*/
-
     public void didChangeState(EnumType.CallState state) {
 
     }
@@ -316,13 +251,6 @@ public abstract class SingleCallFragment extends Fragment {
     public void didReceiveRemoteVideoTrack(BigInteger userId) {
     }
 
-    private void refreshMessage(Boolean isForCallTime) {
-        if (callSingleActivity == null) {
-            return;
-        }
-        // 刷新消息; demo中没有消息,不用处理这儿快逻辑
-    }
-
     public void startRefreshTime() {
         if (durationTextView != null) {
             leftTime = 0;

+ 366 - 0
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/ui/SipCallActivity.java

@@ -0,0 +1,366 @@
+package com.wdkl.ncs.android.component.home.ui;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import androidx.fragment.app.FragmentManager;
+
+import com.enation.javashop.utils.base.tool.CommonTool;
+import com.google.gson.Gson;
+import com.wdkl.ncs.android.component.home.R;
+import com.wdkl.ncs.android.component.home.service.FloatingService;
+import com.wdkl.ncs.android.component.home.settingconfig.SettingConfig;
+import com.wdkl.ncs.android.component.home.util.ActivityStackUtil;
+import com.wdkl.ncs.android.component.home.util.HomeWatcher;
+import com.wdkl.ncs.android.component.home.util.LocaleMangerUtils;
+import com.wdkl.ncs.android.component.home.util.MediaPlayHelper;
+import com.wdkl.ncs.android.component.home.util.RecordHelper;
+import com.wdkl.ncs.android.component.home.util.RingPlayHelper;
+import com.wdkl.ncs.android.component.home.util.SpeechUtil;
+import com.wdkl.ncs.android.lib.base.BaseApplication;
+import com.wdkl.ncs.android.middleware.common.Constants;
+import com.wdkl.ncs.android.middleware.model.vo.InteractionVO;
+import com.wdkl.ncs.android.middleware.tcp.TcpClient;
+import com.wdkl.ncs.android.middleware.tcp.channel.DeviceChannel;
+import com.wdkl.ncs.android.middleware.tcp.channel.VoiceUtil;
+import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel;
+import com.wdkl.ncs.android.middleware.tcp.enums.DeviceTypeEnum;
+import com.wdkl.ncs.android.middleware.tcp.enums.TcpAction;
+import com.wdkl.ncs.android.middleware.tcp.enums.TcpType;
+import com.wdkl.ncs.host.sip.core.LinphoneManager;
+
+import org.linphone.core.Call;
+
+import java.math.BigInteger;
+
+import pub.devrel.easypermissions.EasyPermissions;
+
+/**
+ * SIP通话界面
+ */
+public class SipCallActivity extends BaseCallActivity {
+
+    private static final String TAG = "SipCallActivity";
+
+    private boolean isOutgoing;
+    private BigInteger roomId;
+
+    boolean isAudioOnly = true;
+    boolean isSimulateBed = false;
+    private BaseSipCallFragment currentFragment;
+
+    private boolean mServiceBound = false;
+    private HomeWatcher mHomeWatcher;
+
+    private ServiceConnection mCallServiceConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.d(TAG, "floating service connected");
+            // 获取服务的操作对象
+            FloatingService.MyBinder binder = (FloatingService.MyBinder) service;
+            binder.getService();
+        }
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            Log.d(TAG, "floating service disconnected");
+        }
+    };
+
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        //切换语言
+        int languageId = SettingConfig.getLanguageId(this);
+        LocaleMangerUtils.setApplicationLanguageByIndex(this, languageId);
+
+        super.onCreate(savedInstanceState);
+        setStatusBarOrScreenStatus(this);
+        setContentView(R.layout.activity_single_call);
+
+        DeviceChannel.calling = true;
+        linphoneManager = LinphoneManager.Companion.getInstance(BaseApplication.appContext);
+
+        mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+        loadingDialog = CommonTool.createLoadingDialog(this, R.layout.custom_loading,R.id.loadding_image);
+
+        final Intent intent = getIntent();
+        roomId = new BigInteger(intent.getStringExtra(EXTRA_ROOM_ID));
+        isOutgoing = intent.getBooleanExtra(EXTRA_MO, false);
+        isAudioOnly = intent.getBooleanExtra(EXTRA_AUDIO_ONLY,true);
+        recTcpModel = (TcpModel) intent.getSerializableExtra(EXTRA_TCPMODEL);
+        showName = intent.getStringExtra(EXTRA_SHOWNAME);
+        ringUp = intent.getBooleanExtra(EXTRA_RING, true);
+
+        if (recTcpModel !=null && recTcpModel.getData() != null) {
+            Log.d(TAG, recTcpModel.toJson());
+            InteractionVO interactionVO = new Gson().fromJson(recTcpModel.getData().toString(), InteractionVO.class);
+            isSimulateBed = (interactionVO.getFromDeviceType() == DeviceTypeEnum.SIMULATE_BED_DEVICE.value());
+        }
+
+        // 权限检测
+        String[] perms;
+        if (isAudioOnly) {
+            perms = new String[]{Manifest.permission.RECORD_AUDIO};
+        } else {
+            perms = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+        }
+        if (!EasyPermissions.hasPermissions(SipCallActivity.this, perms)) {
+            EasyPermissions.requestPermissions(SipCallActivity.this, "need record, camera permissions",
+                    100, perms);
+        } else {
+            init(roomId, isAudioOnly);
+        }
+
+        RecordHelper.getInstance().stopCancelRecordByOther(true);
+
+        //home和recent按键事件监听
+        mHomeWatcher = new HomeWatcher(this);
+        mHomeWatcher.setOnHomePressedListener(new HomeWatcher.OnHomePressedListener() {
+            @Override
+            public void onHomePressed() {
+                //如果悬浮窗没有显示 就开启服务展示悬浮窗
+                if (!Constants.Companion.getShowFloatWindow()) {
+                    startFloatingService();
+                }
+            }
+
+            @Override
+            public void onRecentAppPressed() {
+                //如果悬浮窗没有显示 就开启服务展示悬浮窗
+                if (!Constants.Companion.getShowFloatWindow()) {
+                    startFloatingService();
+                }
+            }
+        });
+        mHomeWatcher.startWatch();
+
+        ActivityStackUtil.setCallSingleActivity(this);
+    }
+
+    private void startFloatingService() {
+        //先检查权限
+        if (Build.VERSION.SDK_INT >= 23) {
+            if (!Settings.canDrawOverlays(this)) {
+                Toast.makeText(SipCallActivity.this, "need floating window permission", Toast.LENGTH_LONG).show();
+                return;
+            }
+        }
+
+        //最小化Activity
+        moveTaskToBack(true);
+
+        //开启服务显示悬浮框
+        Intent floatVideoIntent = new Intent(this, FloatingService.class);
+        mServiceBound = bindService(floatVideoIntent, mCallServiceConnection, Context.BIND_AUTO_CREATE);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        //界面恢复,移除悬浮框
+        if (mServiceBound) {
+            unbindService(mCallServiceConnection);
+            mServiceBound = false;
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        //
+    }
+
+    public BaseSipCallFragment getCurrentFragment() {
+        return currentFragment;
+    }
+
+    public Vibrator getmVibrator() {
+        return mVibrator;
+    }
+
+    private void init(BigInteger roomId, boolean audioOnly) {
+        BaseSipCallFragment fragment = new FragmentSipAudio();
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        currentFragment = fragment;
+        fragmentManager.beginTransaction()
+                    .replace(R.id.fragment_content, fragment)
+                    .commitAllowingStateLoss();
+
+        /*if (!isOutgoing) {
+            long[] pattern = new long[]{100L, 2000L, 1000L, 2000L, 1000L, 2000L};
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                mVibrator.vibrate(VibrationEffect.createWaveform(pattern, -1));
+            } else {
+                mVibrator.vibrate(pattern, -1);
+            }
+        }*/
+    }
+
+    @Override
+    public void callOutSuccess(TcpModel tcpModel) {
+        if (currentFragment != null) {
+            currentFragment.callOutSuccess(tcpModel);
+        }
+    }
+
+    @TargetApi(19)
+    private static int getSystemUiVisibility() {
+        int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN |
+                View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+        }
+        return flags;
+    }
+
+    /**
+     * 设置状态栏透明
+     */
+    @TargetApi(19)
+    public void setStatusBarOrScreenStatus(Activity activity) {
+        Window window = activity.getWindow();
+        //全屏+锁屏+常亮显示
+        window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN |
+                //WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
+                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
+                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+        window.getDecorView().setSystemUiVisibility(getSystemUiVisibility());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
+            layoutParams.layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+            window.setAttributes(layoutParams);
+        }
+        // 5.0以上系统状态栏透明
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            //清除透明状态栏
+            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+            //设置状态栏颜色必须添加
+            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+            window.setStatusBarColor(Color.TRANSPARENT);//设置透明
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //19
+            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+        }
+    }
+
+    @SuppressLint("CheckResult")
+    public void sendHandOffTcp(){
+        if (recTcpModel.getType() == TcpType.VOICE) {
+            InteractionVO interactionVO = null;
+            if (recTcpModel.getData()!=null){
+                interactionVO = new Gson().fromJson(recTcpModel.getData().toString(), InteractionVO.class);
+            }
+
+            if (isOutgoing) {
+                //去电
+                if (interactionVO != null) {
+                    int toId;
+                    if (interactionVO.getFromDeviceId().equals(Constants.Companion.getDeviceId())) {
+                        toId = interactionVO.getToDeviceId();
+                    } else {
+                        toId = interactionVO.getFromDeviceId();
+                    }
+
+                    if (recTcpModel.getAction() == TcpAction.VoiceAction.SUCCESS) {
+                        //呼叫成功后取消呼叫
+                        TcpModel voiceUtilTcpModel = VoiceUtil.voiceCancel(recTcpModel.getTid(), Constants.Companion.getDeviceId(), toId, interactionVO.getId());
+                        TcpClient.getInstance().sendMsg(voiceUtilTcpModel.toJson());
+                    } else if (recTcpModel.getAction() == TcpAction.VoiceAction.ACCEPT) {
+                        //对方接听了挂断
+                        TcpModel voiceUtilTcpModel = VoiceUtil.voiceHandoff(recTcpModel.getTid(), Constants.Companion.getDeviceId(), toId, interactionVO.getId());
+                        TcpClient.getInstance().sendMsg(voiceUtilTcpModel.toJson());
+                    }
+                }
+            } else if (recTcpModel.getAction() == TcpAction.VoiceAction.CALL || recTcpModel.getAction() == TcpAction.VoiceAction.HCALL) {
+                //来电
+                if (interactionVO != null) {
+                    int toId;
+                    if (interactionVO.getFromDeviceId().equals(Constants.Companion.getDeviceId())) {
+                        toId = interactionVO.getToDeviceId();
+                    } else {
+                        toId = interactionVO.getFromDeviceId();
+                    }
+
+                    if (callConnected) {
+                        TcpModel voiceUtilTcpModel = VoiceUtil.voiceHandoff(recTcpModel.getTid(), Constants.Companion.getDeviceId(), toId, interactionVO.getId());
+                        TcpClient.getInstance().sendMsg(voiceUtilTcpModel.toJson());
+                    } else {
+                        TcpModel voiceUtilTcpModel = VoiceUtil.voiceReject(recTcpModel.getTid(), Constants.Companion.getDeviceId(), toId, interactionVO.getId());
+                        TcpClient.getInstance().sendMsg(voiceUtilTcpModel.toJson());
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event){
+        return currentFragment.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public synchronized void finish(){
+        Log.e(TAG, "call activity end");
+
+        linphoneManager.terminateCall();
+
+        if (loadingDialog != null) {
+            loadingDialog.dismiss();
+        }
+
+        if (mVibrator != null) {
+            mVibrator.cancel();
+        }
+
+        if (!isFinishing()) {
+            super.finish();
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.d(TAG, "call activity destroyed");
+        MediaPlayHelper.getInstance().stopMusic(true);
+        RingPlayHelper.stopRingTone();
+        SpeechUtil.getInstance().stopSpeak();
+        if (mHomeWatcher != null) {
+            mHomeWatcher.stopWatch();
+        }
+
+        DeviceChannel.calling = false;
+        DeviceChannel.callId = 0;
+
+        //解绑,不显示悬浮框
+        if (mServiceBound) {
+            unbindService(mCallServiceConnection);
+            mServiceBound = false;
+        }
+
+        ActivityStackUtil.setCallSingleActivity(null);
+
+        super.onDestroy();
+    }
+
+    public boolean isOutgoing() {
+        return isOutgoing;
+    }
+}

+ 4 - 4
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/util/ActivityStackUtil.java

@@ -1,15 +1,15 @@
 package com.wdkl.ncs.android.component.home.util;
 
-import com.wdkl.ncs.android.component.home.ui.CallSingleActivity;
+import com.wdkl.ncs.android.component.home.ui.BaseCallActivity;
 
 public class ActivityStackUtil {
-    private static CallSingleActivity callSingleActivity = null;
+    private static BaseCallActivity callSingleActivity = null;
 
-    public static void setCallSingleActivity(CallSingleActivity activity) {
+    public static void setCallSingleActivity(BaseCallActivity activity) {
         callSingleActivity = activity;
     }
 
-    public static CallSingleActivity getCallSingleActivity() {
+    public static BaseCallActivity getCallSingleActivity() {
         return callSingleActivity;
     }
 }

+ 0 - 64
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/util/HandleTcpConnect.kt.bak

@@ -1,64 +0,0 @@
-package com.wdkl.ncs.android.component.home.util
-
-import android.annotation.SuppressLint
-import android.util.Log
-import com.wdkl.ncs.android.middleware.common.Constants
-import com.wdkl.ncs.android.lib.utils.showMessage
-import com.wdkl.ncs.android.lib.vo.MessageEvent
-import com.wdkl.ncs.android.middleware.tcp.NettyClient
-import org.greenrobot.eventbus.EventBus
-
-class HandleTcpConnect {
-    val TAG = HandleTcpConnect::class.simpleName
-
-    /*伴生对象*/
-    companion object {
-        var instance = HandleTcpConnect()
-    }
-
-    @SuppressLint("CheckResult")
-    fun tcpInitConnect(){
-        Log.d(TAG, "TCP.开始初始连接")
-        NettyClient.instance.connect(Constants.tcpServer, Constants.tcpPort, Constants.heartBeat.toLong()).subscribe {
-            if (it){
-                Log.d(TAG, "TCP.连接成功")
-                EventBus.getDefault().post(MessageEvent("tcp connected", Constants.EVENT_TCP_CONNECTED))
-            } else {
-                Log.e(TAG, "TCP.连接失败")
-                EventBus.getDefault().post(MessageEvent("tcp connected", Constants.EVENT_TCP_BREAK))
-            }
-        }
-    }
-
-    @SuppressLint("CheckResult")
-    fun tcpReConnect(){
-        Log.d(TAG, "TCP.开始重新连接")
-        NettyClient.instance.reConnect().doOnError {
-            it.message?.let { it1 -> Log.e(TAG, it1) }
-        }.subscribe {
-            if (it){
-                Log.d(TAG, "TCP.重连成功")
-            } else {
-                Log.e(TAG, "TCP.重连失败")
-            }
-        }
-    }
-
-    @SuppressLint("CheckResult")
-    fun tcpReConnectWithMsgShow(){
-        Log.d(TAG, "TCP.开始重新连接")
-        //showMessage("服务连接失败,正在重连")
-        NettyClient.instance.reConnect().doOnError {
-            it.message?.let { it1 -> Log.e(TAG, it1) }
-        }.subscribe{
-            if (it){
-                Log.d(TAG, "服务重连成功")
-                //EventBus.getDefault().post(MessageEvent("tcp connected", Constants.EVENT_TCP_CONNECTED))
-                //showMessage("重连成功")
-            } else {
-                Log.d(TAG, "服务重连失败")
-                //showMessage("重连失败,请尝试重启APP")
-            }
-        }
-    }
-}

+ 246 - 0
android_mobile/src/main/yd_w_xiaomi_2_chile/code/com/wdkl/ncs/android/component/home/util/VoiceManagerUtil.java

@@ -0,0 +1,246 @@
+package com.wdkl.ncs.android.component.home.util;
+
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHeadset;
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.os.Build;
+
+/**
+ * 类名称:VoiceManagerUtil <br>
+ * 类描述:声音控制工具类 <br>
+ * 创建人:Waderson Shll (TEL:15675117662)<br>
+ * 创建时间:2018-03-15 <br>
+ * 特别提醒:如有需要该类可任意创建与调用;在未通知本人的情况下该类禁止任何修改!<br>
+ */
+public class VoiceManagerUtil {
+    /**
+     * 获取提示音音量最大值
+     *
+     * @param context
+     */
+    public static int getAlarmMax(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_ALARM);
+    }
+
+    /**
+     * 获取提示音音量当前值
+     *
+     * @param context
+     */
+    public static int getAlarmNow(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM);
+    }
+
+    /**
+     * 获取多媒体音量最大值
+     *
+     * @param context
+     */
+    public static int getMusicMax(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
+    }
+
+    /**
+     * 获取多媒体音量当前值
+     *
+     * @param context
+     */
+    public static int getMusicNow(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+    }
+
+    /**
+     * 获取铃声音量最大值
+     *
+     * @param context
+     */
+    public static int getRingMax(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
+    }
+
+    /**
+     * 获取铃声音量当前值
+     *
+     * @param context
+     */
+    public static int getRingNow(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
+    }
+
+    /**
+     * 获取系统音量最大值
+     *
+     * @param context
+     */
+    public static int getSystemMax(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_SYSTEM);
+    }
+
+    /**
+     * 获取系统音量当前值
+     *
+     * @param context
+     */
+    public static int getSystemNow(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamVolume(AudioManager.STREAM_SYSTEM);
+    }
+
+    /**
+     * 获取通话音量最大值
+     *
+     * @param context
+     */
+    public static int getCallMax(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL);
+    }
+
+    public static int getCallVolume(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
+    }
+
+    /**
+     * 获取通话音量当前值
+     *
+     * @param context
+     */
+    public static int getCallNow(Context context) {
+        AudioManager mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        return mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
+    }
+
+    /**
+     * 设置提示音音量
+     *
+     * @param context
+     * @param percent (百分比;只能0--100之间)
+     */
+    public static void setAlarmVoice(Context context, int percent) {
+        float vPercent=((float)percent)/100f;
+        vPercent = vPercent < 0 ? 0 : vPercent;
+        vPercent = vPercent > 1 ? 1 : vPercent;
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setStreamVolume(AudioManager.STREAM_ALARM, (int) (getAlarmMax(context) * vPercent), 0);
+    }
+
+    /**
+     * 设置多媒体音量
+     *
+     * @param context
+     * @param percent (百分比;只能0--100之间)
+     */
+    public static void setMusicVoice(Context context, int percent) {
+        float vPercent=((float)percent)/100f;
+        vPercent = vPercent < 0 ? 0 : vPercent;
+        vPercent = vPercent > 1 ? 1 : vPercent;
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (int) (getMusicMax(context) * vPercent), 0);
+    }
+
+    /**
+     * 设置铃声音量
+     *
+     * @param context
+     * @param percent (百分比;只能0--100之间)
+     */
+    public static void setRingVoice(Context context, int percent) {
+        float vPercent=((float)percent)/100f;
+        vPercent = vPercent < 0 ? 0 : vPercent;
+        vPercent = vPercent > 1 ? 1 : vPercent;
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setStreamVolume(AudioManager.STREAM_RING, (int) (getRingMax(context) * vPercent), 0);
+    }
+
+    /**
+     * 设置系统音量
+     *
+     * @param context
+     * @param percent (百分比;只能0--100之间)
+     */
+    public static void setSystemVoice(Context context, int percent) {
+        float vPercent=((float)percent)/100f;
+        vPercent = vPercent < 0 ? 0 : vPercent;
+        vPercent = vPercent > 1 ? 1 : vPercent;
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, (int) (getSystemMax(context) * vPercent), 0);
+    }
+
+    /**
+     * 设置通话音量
+     *
+     * @param context
+     * @param percent (百分比;只能0--100之间)
+     */
+    public static void setCallVoice(Context context, int percent) {
+        float vPercent=((float)percent)/100f;
+        vPercent = vPercent < 0 ? 0 : vPercent;
+        vPercent = vPercent > 1 ? 1 : vPercent;
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL, (int) (getCallMax(context) * vPercent), 0);
+    }
+
+    public static void setSpeakerOn(Context context, boolean speakerOn) {
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setSpeakerphoneOn(speakerOn);
+    }
+
+    public static void toggleHeadset(Context context, boolean isHeadset) {
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        if (audioManager != null) {
+            if (isHeadset) {
+                audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+                audioManager.setSpeakerphoneOn(false);
+            } else {
+                audioManager.setSpeakerphoneOn(true);
+            }
+        }
+    }
+
+    public static void setAudioMode(Context context, int mode) {
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setMode(mode);
+    }
+
+    public static void setAudioMute(Context context, boolean mute) {
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setMicrophoneMute(mute);
+    }
+
+    /**
+     * 蓝牙是否连接
+     * @return
+     */
+    public static boolean isBluetoothHeadsetConnected(){
+        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        return (bluetoothAdapter != null && bluetoothAdapter.isEnabled()
+                && bluetoothAdapter.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothHeadset.STATE_CONNECTED);
+    }
+
+    public static boolean isHeadphonesPlugged(Context context) {
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            @SuppressLint("WrongConstant") AudioDeviceInfo[] audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
+            for (AudioDeviceInfo deviceInfo : audioDevices) {
+                if (deviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
+                        || deviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
+                    return true;
+                }
+            }
+            return false;
+        } else {
+            return audioManager.isWiredHeadsetOn();
+        }
+    }
+}

+ 42 - 20
android_mobile/src/main/yd_w_xiaomi_2_chile/res/layout/user_setting_layout.xml

@@ -37,7 +37,7 @@
                     android:orientation="horizontal">
                     <TextView
                         android:text="@string/version_title"
-                        android:textSize="14dp"
+                        android:textSize="14sp"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"/>
@@ -48,7 +48,7 @@
                         android:gravity="right">
                         <TextView
                             android:id="@+id/tv_app_version"
-                            android:textSize="14dp"
+                            android:textSize="14sp"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"/>
                     </LinearLayout>
@@ -60,7 +60,7 @@
                     android:orientation="horizontal">
                     <TextView
                         android:text="@string/device_id"
-                        android:textSize="14dp"
+                        android:textSize="14sp"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"/>
@@ -71,7 +71,7 @@
                         android:gravity="right">
                         <TextView
                             android:id="@+id/tv_device_id"
-                            android:textSize="14dp"
+                            android:textSize="14sp"
                             android:textColor="@color/javashop_color_searcher_tv_gray"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"/>
@@ -84,7 +84,7 @@
                     android:orientation="horizontal">
                     <TextView
                         android:text="@string/device_identifier"
-                        android:textSize="14dp"
+                        android:textSize="14sp"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"/>
@@ -95,7 +95,7 @@
                         android:gravity="right">
                         <TextView
                             android:id="@+id/tv_device_imei"
-                            android:textSize="14dp"
+                            android:textSize="14sp"
                             android:textColor="@color/javashop_color_searcher_tv_gray"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"/>
@@ -108,7 +108,7 @@
                     android:orientation="horizontal">
                     <TextView
                         android:text="@string/server_title"
-                        android:textSize="14dp"
+                        android:textSize="14sp"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"/>
@@ -119,7 +119,7 @@
                         android:gravity="right">
                         <TextView
                             android:id="@+id/tv_server_ip"
-                            android:textSize="14dp"
+                            android:textSize="14sp"
                             android:textColor="@color/javashop_color_searcher_tv_gray"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"/>
@@ -132,7 +132,7 @@
                     android:orientation="horizontal">
                     <TextView
                         android:text="@string/device_ip"
-                        android:textSize="14dp"
+                        android:textSize="14sp"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"/>
@@ -143,7 +143,7 @@
                         android:gravity="right">
                         <TextView
                             android:id="@+id/tv_device_ip"
-                            android:textSize="14dp"
+                            android:textSize="14sp"
                             android:textColor="@color/javashop_color_searcher_tv_gray"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"/>
@@ -161,7 +161,7 @@
                         android:layout_height="wrap_content"
                         android:text="@string/tts_state"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
-                        android:textSize="14dp" />
+                        android:textSize="14sp" />
 
                     <TextView
                         android:id="@+id/tv_tts_status"
@@ -170,7 +170,7 @@
                         android:layout_weight="1"
                         android:gravity="right"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
-                        android:textSize="14dp" />
+                        android:textSize="14sp" />
                 </LinearLayout>
 
                 <LinearLayout
@@ -183,7 +183,7 @@
                         android:layout_height="wrap_content"
                         android:text="@string/call_type"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
-                        android:textSize="14dp" />
+                        android:textSize="14sp" />
 
                     <TextView
                         android:id="@+id/tv_phone_type"
@@ -192,7 +192,29 @@
                         android:layout_weight="1"
                         android:gravity="right"
                         android:textColor="@color/javashop_color_searcher_tv_gray"
-                        android:textSize="14dp" />
+                        android:textSize="14sp" />
+                </LinearLayout>
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:text="@string/voice_type"
+                        android:textColor="@color/javashop_color_searcher_tv_gray"
+                        android:textSize="14sp" />
+
+                    <TextView
+                        android:id="@+id/tv_voice_type"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content"
+                        android:layout_weight="1"
+                        android:gravity="right"
+                        android:textColor="@color/javashop_color_searcher_tv_gray"
+                        android:textSize="14sp" />
                 </LinearLayout>
 
             </LinearLayout>
@@ -218,7 +240,7 @@
                     android:layout_gravity="center_horizontal"
                     android:background="@drawable/javashop_btn_balck_line_bg"
                     android:text="@string/str_user_change"
-                    android:textSize="14dp" />
+                    android:textSize="14sp" />
 
                 <Button
                     android:id="@+id/btn_app_restart"
@@ -229,7 +251,7 @@
                     android:layout_toRightOf="@id/view_center_base"
                     android:background="@drawable/javashop_btn_balck_line_bg"
                     android:text="@string/register_restart"
-                    android:textSize="14dp" />
+                    android:textSize="14sp" />
 
                 <Button
                     android:id="@+id/btn_check_update"
@@ -242,7 +264,7 @@
                     android:layout_below="@id/btn_user_change"
                     android:background="@drawable/javashop_btn_balck_line_bg"
                     android:text="@string/check_update"
-                    android:textSize="14dp" />
+                    android:textSize="14sp" />
 
                 <Button
                     android:id="@+id/btn_system_setting"
@@ -255,7 +277,7 @@
                     android:layout_toRightOf="@id/view_center_base"
                     android:background="@drawable/javashop_btn_balck_line_bg"
                     android:text="@string/str_system_settings"
-                    android:textSize="14dp" />
+                    android:textSize="14sp" />
 
                 <Button
                     android:id="@+id/btn_update_contact"
@@ -268,7 +290,7 @@
                     android:layout_below="@id/btn_system_setting"
                     android:background="@drawable/javashop_btn_balck_line_bg"
                     android:text="@string/contatc_update_title"
-                    android:textSize="14dp" />
+                    android:textSize="14sp" />
 
                 <Button
                     android:id="@+id/btn_phone_number"
@@ -281,7 +303,7 @@
                     android:layout_toRightOf="@id/view_center_base"
                     android:background="@drawable/javashop_btn_balck_line_bg"
                     android:text="@string/phone_number_title"
-                    android:textSize="14dp" />
+                    android:textSize="14sp" />
 
                 <TextView
                     android:id="@+id/tv_call_title"

+ 0 - 1
android_mobile/src/main/yd_watch_2/code/com/wdkl/ncs/android/component/home/ui/BaseSipCallFragment.java

@@ -31,7 +31,6 @@ import com.wdkl.ncs.android.component.home.R;
 import com.wdkl.ncs.android.component.home.util.MediaPlayHelper;
 import com.wdkl.ncs.android.component.home.util.RingPlayHelper;
 import com.wdkl.ncs.android.component.home.util.SpeechUtil;
-import com.wdkl.ncs.android.component.home.util.VoiceManagerUtil;
 import com.wdkl.ncs.android.lib.base.BaseApplication;
 import com.wdkl.ncs.android.lib.utils.ExtendMethodsKt;
 import com.wdkl.ncs.android.lib.vo.MessageEvent;

+ 0 - 1
android_mobile/src/main/yd_watch_2/code/com/wdkl/ncs/android/component/home/ui/FragmentSipAudio.java

@@ -24,7 +24,6 @@ import com.wdkl.ncs.android.component.home.util.MediaPlayHelper;
 import com.wdkl.ncs.android.component.home.util.RingPlayHelper;
 import com.wdkl.ncs.android.component.home.util.SpeechUtil;
 import com.wdkl.ncs.android.component.home.util.Util;
-import com.wdkl.ncs.android.component.home.util.VoiceManagerUtil;
 import com.wdkl.ncs.android.lib.utils.ExtendMethodsKt;
 import com.wdkl.ncs.android.middleware.common.Constants;
 import com.wdkl.ncs.android.middleware.model.vo.InteractionVO;

+ 7 - 2
app/src/main/AndroidManifest.xml

@@ -25,6 +25,9 @@
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
 
+    <!-- Needed for full screen intent in incoming call notifications -->
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
+
     <application
         android:allowBackup="true"
         android:icon="@drawable/ic_nurse_launch"
@@ -79,8 +82,10 @@
         <activity android:name="com.wdkl.ncs.host.activity.CallActivity" />
 
         <service
-            android:name="com.wdkl.ncs.host.service.WdklSipService"
-            android:label="@string/app_name" />
+            android:name="com.wdkl.ncs.host.sip.core.LinCoreService"
+            android:foregroundServiceType="phoneCall|camera|microphone"
+            android:label="@string/app_name"
+            android:stopWithTask="false" />
 
         <meta-data
             android:name="android.max_aspect"

+ 1 - 1
common/build.gradle

@@ -147,7 +147,7 @@ dependencies {
     compile files('libs/chinese2py.jar')
 
     //linphone sip sdk
-    api(name: 'linphone-sdk-android-5.2.10', ext: 'aar')
+    api(name: 'linphone-sdk-android-5.0.71', ext: 'aar')
 
     //toast 框架
     api(name: 'Toaster-12.6', ext: 'aar')

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