weizhengliang hace 2 años
padre
commit
07bd72889c
Se han modificado 100 ficheros con 6195 adiciones y 0 borrados
  1. 1 0
      .gitignore
  2. 5 0
      WebRTC/.gitignore
  3. 85 0
      WebRTC/build.gradle
  4. 25 0
      WebRTC/proguard-rules.pro
  5. 55 0
      WebRTC/src/main/AndroidManifest.xml
  6. 23 0
      WebRTC/src/main/java/com/wdkl/core/base/BaseActivity.java
  7. 32 0
      WebRTC/src/main/java/com/wdkl/core/consts/Urls.java
  8. 48 0
      WebRTC/src/main/java/com/wdkl/core/socket/IEvent.java
  9. 15 0
      WebRTC/src/main/java/com/wdkl/core/socket/IUserState.java
  10. 509 0
      WebRTC/src/main/java/com/wdkl/core/socket/MyWebSocket.java
  11. 404 0
      WebRTC/src/main/java/com/wdkl/core/socket/SocketManager.java
  12. 35 0
      WebRTC/src/main/java/com/wdkl/core/ui/event/MsgEvent.java
  13. 42 0
      WebRTC/src/main/java/com/wdkl/core/ui/user/UserBean.java
  14. 53 0
      WebRTC/src/main/java/com/wdkl/core/ui/user/UserListViewModel.java
  15. 131 0
      WebRTC/src/main/java/com/wdkl/core/util/ActivityStackManager.java
  16. 58 0
      WebRTC/src/main/java/com/wdkl/core/util/CrashHandler.java
  17. 127 0
      WebRTC/src/main/java/com/wdkl/core/util/OSUtils.java
  18. 25 0
      WebRTC/src/main/java/com/wdkl/core/util/StringUtil.java
  19. 52 0
      WebRTC/src/main/java/com/wdkl/core/util/Utils.java
  20. 192 0
      WebRTC/src/main/java/com/wdkl/core/voip/AsyncPlayer.java
  21. 129 0
      WebRTC/src/main/java/com/wdkl/core/voip/CallForegroundNotification.java
  22. 248 0
      WebRTC/src/main/java/com/wdkl/core/voip/CallMultiActivity.java
  23. 394 0
      WebRTC/src/main/java/com/wdkl/core/voip/CallSingleActivity.java
  24. 150 0
      WebRTC/src/main/java/com/wdkl/core/voip/FragmentAudio.java
  25. 118 0
      WebRTC/src/main/java/com/wdkl/core/voip/FragmentMeeting.java
  26. 317 0
      WebRTC/src/main/java/com/wdkl/core/voip/FragmentVideo.java
  27. 73 0
      WebRTC/src/main/java/com/wdkl/core/voip/NineGridView.java
  28. 153 0
      WebRTC/src/main/java/com/wdkl/core/voip/RomUtil.java
  29. 265 0
      WebRTC/src/main/java/com/wdkl/core/voip/SettingsCompat.java
  30. 321 0
      WebRTC/src/main/java/com/wdkl/core/voip/SingleCallFragment.java
  31. 7 0
      WebRTC/src/main/java/com/wdkl/core/voip/Utils.java
  32. 111 0
      WebRTC/src/main/java/com/wdkl/core/voip/VoipEvent.java
  33. 232 0
      WebRTC/src/main/java/com/wdkl/core/voip/VoipReceiver.java
  34. 37 0
      WebRTC/src/main/java/com/wdkl/net/HttpRequest.java
  35. 47 0
      WebRTC/src/main/java/com/wdkl/net/HttpRequestPresenter.java
  36. 12 0
      WebRTC/src/main/java/com/wdkl/net/ICallback.java
  37. 54 0
      WebRTC/src/main/java/com/wdkl/net/urlconn/UrlConnRequest.java
  38. 263 0
      WebRTC/src/main/java/com/wdkl/net/urlconn/UrlConnUtils.java
  39. 18 0
      WebRTC/src/main/java/com/wdkl/permission/Consumer.java
  40. 144 0
      WebRTC/src/main/java/com/wdkl/permission/Permissions.java
  41. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_audio_answer.png
  42. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_audio_answer_hover.png
  43. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_camera.png
  44. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_camera_hover.png
  45. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_default_header.png
  46. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_float_audio.png
  47. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_handfree.png
  48. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_handfree_hover.png
  49. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_hang_up.png
  50. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_hang_up_hover.png
  51. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_minimize.png
  52. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_mute.png
  53. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_mute_hover.png
  54. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_phone.png
  55. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_trans_audio.png
  56. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_video_answer.png
  57. BIN
      WebRTC/src/main/res/drawable-xhdpi/av_video_answer_hover.png
  58. 20 0
      WebRTC/src/main/res/drawable-xhdpi/bg_btn_white.xml
  59. 10 0
      WebRTC/src/main/res/drawable/av_audio_answer_selector.xml
  60. 16 0
      WebRTC/src/main/res/drawable/av_float_bg.xml
  61. 10 0
      WebRTC/src/main/res/drawable/av_hangup_selector.xml
  62. 5 0
      WebRTC/src/main/res/drawable/av_mute_selector.xml
  63. 5 0
      WebRTC/src/main/res/drawable/av_speaker_selector.xml
  64. 10 0
      WebRTC/src/main/res/drawable/av_switch_camera_selector.xml
  65. 10 0
      WebRTC/src/main/res/drawable/av_video_answer_selector.xml
  66. 9 0
      WebRTC/src/main/res/drawable/ic_dashboard_black_24dp.xml
  67. 9 0
      WebRTC/src/main/res/drawable/ic_home_black_24dp.xml
  68. 9 0
      WebRTC/src/main/res/drawable/ic_notifications_black_24dp.xml
  69. 52 0
      WebRTC/src/main/res/layout/activity_launcher.xml
  70. 29 0
      WebRTC/src/main/res/layout/activity_main.xml
  71. 21 0
      WebRTC/src/main/res/layout/activity_multi_call.xml
  72. 9 0
      WebRTC/src/main/res/layout/activity_single_call.xml
  73. 64 0
      WebRTC/src/main/res/layout/av_p2p_audio_incoming.xml
  74. 92 0
      WebRTC/src/main/res/layout/av_p2p_audio_outgoing.xml
  75. 113 0
      WebRTC/src/main/res/layout/av_p2p_meeting_action.xml
  76. 90 0
      WebRTC/src/main/res/layout/av_p2p_video_connected_action.xml
  77. 88 0
      WebRTC/src/main/res/layout/av_p2p_video_incoming_action.xml
  78. 54 0
      WebRTC/src/main/res/layout/av_p2p_video_outgoing_action.xml
  79. 39 0
      WebRTC/src/main/res/layout/av_voip_float_view.xml
  80. 76 0
      WebRTC/src/main/res/layout/fragment_audio.xml
  81. 28 0
      WebRTC/src/main/res/layout/fragment_home.xml
  82. 12 0
      WebRTC/src/main/res/layout/fragment_meeting.xml
  83. 27 0
      WebRTC/src/main/res/layout/fragment_room.xml
  84. 25 0
      WebRTC/src/main/res/layout/fragment_setting.xml
  85. 119 0
      WebRTC/src/main/res/layout/fragment_video.xml
  86. 40 0
      WebRTC/src/main/res/layout/item_rooms.xml
  87. 48 0
      WebRTC/src/main/res/layout/item_users.xml
  88. 19 0
      WebRTC/src/main/res/menu/bottom_nav_menu.xml
  89. 12 0
      WebRTC/src/main/res/menu/menu_room.xml
  90. BIN
      WebRTC/src/main/res/mipmap-hdpi/ic_launcher.png
  91. BIN
      WebRTC/src/main/res/mipmap-mdpi/ic_launcher.png
  92. BIN
      WebRTC/src/main/res/mipmap-xhdpi/ic_launcher.png
  93. BIN
      WebRTC/src/main/res/mipmap-xxhdpi/ic_launcher.png
  94. BIN
      WebRTC/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  95. BIN
      WebRTC/src/main/res/raw/incoming_call_ring.mp3
  96. BIN
      WebRTC/src/main/res/raw/outgoing_call_ring.mp3
  97. BIN
      WebRTC/src/main/res/raw/wr_ringback.wav
  98. 9 0
      WebRTC/src/main/res/values/colors.xml
  99. 6 0
      WebRTC/src/main/res/values/dimens.xml
  100. 0 0
      WebRTC/src/main/res/values/strings.xml

+ 1 - 0
.gitignore

@@ -8,3 +8,4 @@
 /gradle
 .externalNativeBuild
 /.gradle
+/.idea

+ 5 - 0
WebRTC/.gitignore

@@ -0,0 +1,5 @@
+/build
+*.iml
+.DS_Store
+/.idea
+/gradle

+ 85 - 0
WebRTC/build.gradle

@@ -0,0 +1,85 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion target_sdk_version
+    buildToolsVersion build_tools_version
+
+    defaultConfig {
+        minSdkVersion min_sdk_version
+        targetSdkVersion target_sdk_version
+        versionCode app_version_code
+        versionName app_version
+
+        compileOptions {
+            sourceCompatibility JavaVersion.VERSION_1_8
+            targetCompatibility JavaVersion.VERSION_1_8
+        }
+        vectorDrawables.useSupportLibrary = true
+
+        //ndk {
+            // 设置支持的SO库架构
+        //    abiFilters 'armeabi-v7a', 'x86'//, 'arm64-v8a'
+        //}
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+
+        debug {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+
+    //implementation 'androidx.appcompat:appcompat:1.2.0'
+    //implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    //implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+    //implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
+    // navigation
+    //implementation 'androidx.navigation:navigation-fragment:2.3.0'
+    //implementation 'androidx.navigation:navigation-ui:2.3.0'
+
+    //implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+
+    implementation 'com.google.android.material:material:1.1.0'
+    /**
+     *  Android基础依赖库
+     */
+    //noinspection GradleCompatible
+    implementation "com.android.support:design:$support_library_version"
+    implementation "com.android.support:support-v4:$support_library_version"
+    implementation "com.android.support:cardview-v7:$support_library_version"
+    implementation "com.android.support:appcompat-v7:$support_library_version"
+
+    implementation "com.android.support.constraint:constraint-layout:1.1.0-beta5"
+    implementation "com.android.support:support-vector-drawable:$support_library_version"
+    implementation "android.arch.lifecycle:extensions:1.0.0-alpha4"
+
+    // 内存泄漏检测
+    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1'
+
+    implementation project(path: ':rtc-chat')
+    // java
+    implementation 'org.java-websocket:Java-WebSocket:1.4.0'
+
+    compile 'com.alibaba:fastjson:1.2.23'
+    //强大的弹窗库
+    implementation 'com.lxj:xpopup:2.2.0'
+    //eventbus
+    //compile 'org.greenrobot:eventbus:3.1.1'
+
+    compile 'com.blankj:utilcodex:1.30.5'
+
+    compile project(':middleware')
+
+    //通知提示弹出库
+    api 'com.tapadoo.android:alerter:6.2.1'
+}

+ 25 - 0
WebRTC/proguard-rules.pro

@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/dongxiangjun/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 55 - 0
WebRTC/src/main/AndroidManifest.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.wdkl.webrtc">
+    <!-- 设置视频直播权限 -->
+    <uses-feature android:name="android.hardware.camera" />
+    <uses-feature android:name="android.hardware.camera.autofocus" />
+    <uses-feature
+        android:glEsVersion="0x00020000"
+        android:required="true" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 悬浮窗显示 -->
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
+
+    <application
+        android:allowBackup="true"
+        android:supportsRtl="true">
+
+        <!-- ======================java====================== -->
+        <activity
+            android:name="com.wdkl.core.voip.CallSingleActivity"
+            android:screenOrientation="landscape"
+            android:showOnLockScreen="true"
+            android:showWhenLocked="true"
+            android:theme="@style/AppTheme.NoActionBar"
+            tools:ignore="UnusedAttribute">
+            <intent-filter>
+                <action android:name="${applicationId}.kit.voip.single" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name="com.wdkl.core.voip.CallMultiActivity"
+            android:showOnLockScreen="true"
+            android:theme="@style/AppTheme.NoActionBar" />
+
+        <receiver android:name="com.wdkl.core.voip.VoipReceiver">
+            <intent-filter>
+                <action android:name="${applicationId}.voip.Receiver" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>

+ 23 - 0
WebRTC/src/main/java/com/wdkl/core/base/BaseActivity.java

@@ -0,0 +1,23 @@
+package com.wdkl.core.base;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+
+
+import com.wdkl.core.util.ActivityStackManager;
+
+public class BaseActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        // 添加Activity到堆栈
+        ActivityStackManager.getInstance().onCreated(this);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected void onDestroy() {
+        ActivityStackManager.getInstance().onDestroyed(this);
+        super.onDestroy();
+    }
+}

+ 32 - 0
WebRTC/src/main/java/com/wdkl/core/consts/Urls.java

@@ -0,0 +1,32 @@
+package com.wdkl.core.consts;
+
+/**
+ * Created by dds on 2020/4/19.
+ * ddssingsong@163.com
+ */
+public class Urls {
+
+    //private final static String IP = "192.168.2.111";
+    //public final static String IP = "42.192.40.58:5000";
+//    public final static String IP = "172.28.100.100:5000";
+
+    public final static String IP = "120.76.246.253:5000";
+
+    private final static String HOST = "http://" + IP + "/";
+
+    // 信令地址
+    public final static String WS = "ws://" + IP + "/ws";
+
+    //用户名称
+    public static String USER_ID;
+
+    // 获取用户列表
+    public static String getUserList() {
+        return HOST + "userList";
+    }
+
+    // 获取房间列表
+    public static String getRoomList() {
+        return HOST + "roomList";
+    }
+}

+ 48 - 0
WebRTC/src/main/java/com/wdkl/core/socket/IEvent.java

@@ -0,0 +1,48 @@
+package com.wdkl.core.socket;
+
+/**
+ * Created by dds on 2019/7/26.
+ * ddssingsong@163.com
+ */
+public interface IEvent {
+
+
+    void onOpen();
+
+    void loginSuccess(String userId, String avatar);
+
+
+    void onInvite(String room, boolean audioOnly, String inviteId, String userList);
+
+
+    void onCancel(String inviteId);
+
+    void onRing(String userId);
+
+
+    void onPeers(String myId, String userList, int roomSize);
+
+    void onNewPeer(String myId);
+
+    void onReject(String userId, int type);
+
+    // onOffer
+    void onOffer(String userId, String sdp);
+
+    // onAnswer
+    void onAnswer(String userId, String sdp);
+
+    // ice-candidate
+    void onIceCandidate(String userId, String id, int label, String candidate);
+
+    void onLeave(String userId);
+
+    void logout(String str);
+
+    void onTransAudio(String userId);
+
+    void onDisConnect(String userId);
+
+    void reConnect();
+
+}

+ 15 - 0
WebRTC/src/main/java/com/wdkl/core/socket/IUserState.java

@@ -0,0 +1,15 @@
+package com.wdkl.core.socket;
+
+/**
+ * Created by dds on 2019/8/2.
+ * android_shuai@163.com
+ */
+public interface IUserState {
+
+
+    void userLogin();
+
+    void userLogout();
+
+
+}

+ 509 - 0
WebRTC/src/main/java/com/wdkl/core/socket/MyWebSocket.java

@@ -0,0 +1,509 @@
+package com.wdkl.core.socket;
+
+import android.annotation.SuppressLint;
+import android.util.Log;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.wdkl.core.util.StringUtil;
+
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.handshake.ServerHandshake;
+
+import java.net.URI;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Created by dds on 2019/7/26.
+ * android_shuai@163.com
+ */
+public class MyWebSocket extends WebSocketClient {
+    private final static String TAG = "dds_WebSocket";
+    private final IEvent iEvent;
+    private boolean connectFlag = false;
+
+
+    public MyWebSocket(URI serverUri, IEvent event) {
+        super(serverUri);
+        this.iEvent = event;
+    }
+
+    @Override
+    public void onClose(int code, String reason, boolean remote) {
+        Log.e("dds_error", "onClose:" + reason + "remote:" + remote);
+        if (connectFlag) {
+            try {
+                Thread.sleep(3000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            this.iEvent.reConnect();
+        } else {
+            this.iEvent.logout("onClose");
+        }
+
+    }
+
+    @Override
+    public void onError(Exception ex) {
+        Log.e("dds_error", "onError:" + ex.toString());
+        this.iEvent.logout("onError");
+        connectFlag = false;
+        /*try {
+            Thread.sleep(3000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        this.iEvent.reConnect();*/
+    }
+
+    @Override
+    public void onOpen(ServerHandshake handshakedata) {
+        Log.e("dds_info", "onOpen");
+        this.iEvent.onOpen();
+        connectFlag = true;
+    }
+
+    @Override
+    public void onMessage(String message) {
+        Log.d(TAG, message);
+        handleMessage(message);
+    }
+
+    public void setConnectFlag(boolean flag) {
+        connectFlag = flag;
+    }
+
+    public boolean isConnectFlag() {
+        return connectFlag;
+    }
+
+    // ---------------------------------------处理接收消息-------------------------------------
+
+    private void handleMessage(String message) {
+        Map map = JSON.parseObject(message, Map.class);
+        String eventName = (String) map.get("eventName");
+        if (eventName == null) return;
+        // 登录成功
+        if (eventName.equals("__login_success")) {
+            handleLogin(map);
+            return;
+        }
+        // 被邀请
+        if (eventName.equals("__invite")) {
+            handleInvite(map);
+            return;
+        }
+        // 取消拨出
+        if (eventName.equals("__cancel")) {
+            handleCancel(map);
+            return;
+        }
+        // 响铃
+        if (eventName.equals("__ring")) {
+            handleRing(map);
+            return;
+        }
+        // 进入房间
+        if (eventName.equals("__peers")) {
+            handlePeers(map);
+            return;
+        }
+        // 新人入房间
+        if (eventName.equals("__new_peer")) {
+            handleNewPeer(map);
+            return;
+        }
+        // 拒绝接听
+        if (eventName.equals("__reject")) {
+            handleReject(map);
+            return;
+        }
+        // offer
+        if (eventName.equals("__offer")) {
+            handleOffer(map);
+            return;
+        }
+        // answer
+        if (eventName.equals("__answer")) {
+            handleAnswer(map);
+            return;
+        }
+        // ice-candidate
+        if (eventName.equals("__ice_candidate")) {
+            handleIceCandidate(map);
+        }
+        // 离开房间
+        if (eventName.equals("__leave")) {
+            handleLeave(map);
+        }
+        // 切换到语音
+        if (eventName.equals("__audio")) {
+            handleTransAudio(map);
+        }
+        // 意外断开
+        if (eventName.equals("__disconnect")) {
+            handleDisConnect(map);
+        }
+
+
+    }
+
+    private void handleDisConnect(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromId = (String) data.get("fromID");
+            this.iEvent.onDisConnect(fromId);
+        }
+    }
+
+    private void handleTransAudio(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromId = (String) data.get("fromID");
+            this.iEvent.onTransAudio(fromId);
+        }
+    }
+
+    private void handleLogin(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String userID = (String) data.get("userID");
+            String avatar = (String) data.get("avatar");
+            this.iEvent.loginSuccess(userID, avatar);
+        }
+
+
+    }
+
+    private void handleIceCandidate(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String userID = (String) data.get("fromID");
+            String id = (String) data.get("id");
+            int label = (int) data.get("label");
+            String candidate = (String) data.get("candidate");
+            this.iEvent.onIceCandidate(userID, id, label, candidate);
+        }
+    }
+
+    private void handleAnswer(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String sdp = (String) data.get("sdp");
+            String userID = (String) data.get("fromID");
+            this.iEvent.onAnswer(userID, sdp);
+        }
+    }
+
+    private void handleOffer(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String sdp = (String) data.get("sdp");
+            String userID = (String) data.get("fromID");
+            this.iEvent.onOffer(userID, sdp);
+        }
+    }
+
+    private void handleReject(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromID = (String) data.get("fromID");
+            int rejectType = Integer.parseInt(String.valueOf(data.get("refuseType")));
+            this.iEvent.onReject(fromID, rejectType);
+        }
+    }
+
+    private void handlePeers(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String you = (String) data.get("you");
+            String connections = (String) data.get("connections");
+            int roomSize = (int) data.get("roomSize");
+            this.iEvent.onPeers(you, connections, roomSize);
+        }
+    }
+
+    private void handleNewPeer(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String userID = (String) data.get("userID");
+            this.iEvent.onNewPeer(userID);
+        }
+    }
+
+    private void handleRing(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromId = (String) data.get("fromID");
+            this.iEvent.onRing(fromId);
+        }
+    }
+
+    private void handleCancel(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String inviteID = (String) data.get("inviteID");
+            String userList = (String) data.get("userList");
+            this.iEvent.onCancel(inviteID);
+        }
+    }
+
+    private void handleInvite(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String room = (String) data.get("room");
+            boolean audioOnly = (boolean) data.get("audioOnly");
+            String inviteID = (String) data.get("inviteID");
+            String userList = (String) data.get("userList");
+            this.iEvent.onInvite(room, audioOnly, inviteID, userList);
+        }
+    }
+
+    private void handleLeave(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromID = (String) data.get("fromID");
+            this.iEvent.onLeave(fromID);
+        }
+    }
+
+    /**
+     * ------------------------------发送消息----------------------------------------
+     */
+    public void createRoom(String room, int roomSize, String myId) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__create");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("roomSize", roomSize);
+        childMap.put("userID", myId);
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 发送邀请
+    public void sendInvite(String room, String myId, List<String> users, boolean audioOnly) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__invite");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("audioOnly", audioOnly);
+        childMap.put("inviteID", myId);
+
+        String join = StringUtil.listToString(users);
+        childMap.put("userList", join);
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 取消邀请
+    public void sendCancel(String mRoomId, String useId, List<String> users) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__cancel");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("inviteID", useId);
+        childMap.put("room", mRoomId);
+
+        String join = StringUtil.listToString(users);
+        childMap.put("userList", join);
+
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 发送响铃通知
+    public void sendRing(String myId, String toId, String room) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__ring");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("fromID", myId);
+        childMap.put("toID", toId);
+        childMap.put("room", room);
+
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    //加入房间
+    public void sendJoin(String room, String myId) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__join");
+
+        Map<String, String> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("userID", myId);
+
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 拒接接听
+    public void sendRefuse(String room, String inviteID, String myId, int refuseType) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__reject");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("toID", inviteID);
+        childMap.put("fromID", myId);
+        childMap.put("refuseType", String.valueOf(refuseType));
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 离开房间
+    public void sendLeave(String myId, String room, String userId) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__leave");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("fromID", myId);
+        childMap.put("userID", userId);
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        if (isOpen()) {
+            send(jsonString);
+        }
+    }
+
+    // send offer
+    public void sendOffer(String myId, String userId, String sdp) {
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("sdp", sdp);
+        childMap.put("userID", userId);
+        childMap.put("fromID", myId);
+        map.put("data", childMap);
+        map.put("eventName", "__offer");
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // send answer
+    public void sendAnswer(String myId, String userId, String sdp) {
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("sdp", sdp);
+        childMap.put("fromID", myId);
+        childMap.put("userID", userId);
+        map.put("data", childMap);
+        map.put("eventName", "__answer");
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // send ice-candidate
+    public void sendIceCandidate(String myId, String userId, String id, int label, String candidate) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__ice_candidate");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("userID", userId);
+        childMap.put("fromID", myId);
+        childMap.put("id", id);
+        childMap.put("label", label);
+        childMap.put("candidate", candidate);
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        if (isOpen()) {
+            send(jsonString);
+        }
+    }
+
+    // 切换到语音
+    public void sendTransAudio(String myId, String userId) {
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("fromID", myId);
+        childMap.put("userID", userId);
+        map.put("data", childMap);
+        map.put("eventName", "__audio");
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 断开重连
+    public void sendDisconnect(String room, String myId, String userId) {
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("fromID", myId);
+        childMap.put("userID", userId);
+        childMap.put("room", room);
+        map.put("data", childMap);
+        map.put("eventName", "__disconnect");
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 忽略证书
+    public static class TrustManagerTest implements X509TrustManager {
+
+        @SuppressLint("TrustAllX509TrustManager")
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+
+        }
+
+        @SuppressLint("TrustAllX509TrustManager")
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[0];
+        }
+    }
+
+}

+ 404 - 0
WebRTC/src/main/java/com/wdkl/core/socket/SocketManager.java

@@ -0,0 +1,404 @@
+package com.wdkl.core.socket;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.wdkl.core.voip.CallSingleActivity;
+import com.wdkl.core.voip.Utils;
+import com.wdkl.core.voip.VoipEvent;
+import com.wdkl.core.voip.VoipReceiver;
+import com.wdkl.ncs.android.component.nursehome.common.Constants;
+import com.wdkl.ncs.android.middleware.utils.MessageEvent;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+
+import org.greenrobot.eventbus.EventBus;
+
+import java.lang.ref.WeakReference;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.SecureRandom;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+
+
+/**
+ * Created by dds on 2019/7/26.
+ * ddssignsong@163.com
+ */
+public class SocketManager implements IEvent {
+    private final static String TAG = "dds_SocketManager";
+    private MyWebSocket webSocket;
+    private int userState;
+    private String myId;
+    private Context mContext;
+
+    private final Handler handler = new Handler(Looper.getMainLooper());
+
+    private SocketManager() {
+
+    }
+
+    public void init(Context context) {
+        mContext = context;
+    }
+
+    private static class Holder {
+        private static final SocketManager socketManager = new SocketManager();
+    }
+
+    public static SocketManager getInstance() {
+        return Holder.socketManager;
+    }
+
+    public boolean getConnectFlag() {
+        if (webSocket != null) {
+            return webSocket.isConnectFlag();
+        }
+        return false;
+    }
+
+    public void connect(String url, String userId, int device) {
+        if (webSocket == null || !webSocket.isOpen()) {
+            URI uri;
+            try {
+                String urls = url + "/" + userId + "/" + device;
+                uri = new URI(urls);
+            } catch (URISyntaxException e) {
+                e.printStackTrace();
+                return;
+            }
+            webSocket = new MyWebSocket(uri, this);
+            // 设置wss
+            if (url.startsWith("wss")) {
+                try {
+                    SSLContext sslContext = SSLContext.getInstance("TLS");
+                    if (sslContext != null) {
+                        sslContext.init(null, new TrustManager[]{new MyWebSocket.TrustManagerTest()}, new SecureRandom());
+                    }
+
+                    SSLSocketFactory factory = null;
+                    if (sslContext != null) {
+                        factory = sslContext.getSocketFactory();
+                    }
+
+                    if (factory != null) {
+                        webSocket.setSocket(factory.createSocket());
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+            // 开始connect
+            webSocket.connect();
+        }
+
+
+    }
+
+    public void unConnect() {
+        if (webSocket != null) {
+            webSocket.setConnectFlag(false);
+            webSocket.close();
+            webSocket = null;
+        }
+
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public void onOpen() {
+        Log.i(TAG, "socket is open!");
+
+    }
+
+    @Override
+    public void loginSuccess(String userId, String avatar) {
+        Log.i(TAG, "loginSuccess:" + userId);
+        myId = userId;
+        userState = 1;
+        if (iUserState != null && iUserState.get() != null) {
+            iUserState.get().userLogin();
+        }
+    }
+
+
+    // ======================================================================================
+    public void createRoom(String room, int roomSize) {
+        if (webSocket != null) {
+            webSocket.createRoom(room, roomSize, myId);
+        }
+
+    }
+
+    public void sendInvite(String room, List<String> users, boolean audioOnly) {
+        if (webSocket != null) {
+            webSocket.sendInvite(room, myId, users, audioOnly);
+        }
+    }
+
+    public void sendLeave(String room, String userId) {
+        if (webSocket != null) {
+            webSocket.sendLeave(myId, room, userId);
+        }
+
+        //EventBus.getDefault().post(new MessageEvent("handoff", EVENT_CALL_STATE));
+    }
+
+    public void sendRingBack(String targetId, String room) {
+        if (webSocket != null) {
+            webSocket.sendRing(myId, targetId, room);
+        }
+    }
+
+    public void sendRefuse(String room, String inviteId, int refuseType) {
+        if (webSocket != null) {
+            webSocket.sendRefuse(room, inviteId, myId, refuseType);
+        }
+
+        //EventBus.getDefault().post(new MessageEvent("reject", EVENT_CALL_STATE));
+    }
+
+    public void sendCancel(String mRoomId, List<String> userIds) {
+        if (webSocket != null) {
+            webSocket.sendCancel(mRoomId, myId, userIds);
+        }
+
+        //EventBus.getDefault().post(new MessageEvent("cancel", EVENT_CALL_STATE));
+    }
+
+    public void sendJoin(String room) {
+        if (webSocket != null) {
+            webSocket.sendJoin(room, myId);
+        }
+
+        //EventBus.getDefault().post(new MessageEvent("accept", EVENT_CALL_STATE));
+    }
+
+    public void sendMeetingInvite(String userList) {
+
+    }
+
+    public void sendOffer(String userId, String sdp) {
+        if (webSocket != null) {
+            webSocket.sendOffer(myId, userId, sdp);
+        }
+    }
+
+    public void sendAnswer(String userId, String sdp) {
+        if (webSocket != null) {
+            webSocket.sendAnswer(myId, userId, sdp);
+        }
+    }
+
+    public void sendIceCandidate(String userId, String id, int label, String candidate) {
+        if (webSocket != null) {
+            webSocket.sendIceCandidate(myId, userId, id, label, candidate);
+        }
+    }
+
+    public void sendTransAudio(String userId) {
+        if (webSocket != null) {
+            webSocket.sendTransAudio(myId, userId);
+        }
+    }
+
+    public void sendDisconnect(String room, String userId) {
+        if (webSocket != null) {
+            webSocket.sendDisconnect(room, myId, userId);
+        }
+    }
+
+
+    // ========================================================================================
+    @Override
+    public void onInvite(String room, boolean audioOnly, String inviteId, String userList) {
+        /*Intent intent = new Intent();
+        intent.putExtra("room", room);
+        intent.putExtra("audioOnly", audioOnly);
+        intent.putExtra("inviteId", inviteId);
+        intent.putExtra("userList", userList);
+        intent.setAction(Utils.ACTION_VOIP_RECEIVER);
+        intent.setComponent(new ComponentName(mContext.getPackageName(), VoipReceiver.class.getName()));
+        // 发送广播
+        mContext.sendBroadcast(intent);*/
+
+        Constants.Companion.setVisitRoomId(room);
+        Constants.Companion.setInviteId(inviteId);
+        SkyEngineKit.init(new VoipEvent());
+        boolean b = SkyEngineKit.Instance().startInCall(mContext, room, inviteId, audioOnly);
+        if (b) {
+            EventBus.getDefault().post(new MessageEvent("visit", Constants.VISIT_MSG));
+        }
+    }
+
+    @Override
+    public void onCancel(String inviteId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onCancel(inviteId);
+            }
+        });
+
+    }
+
+    @Override
+    public void onRing(String fromId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onRingBack(fromId);
+            }
+        });
+
+
+    }
+
+    @Override  // 加入房间
+    public void onPeers(String myId, String connections, int roomSize) {
+        handler.post(() -> {
+            //自己进入了房间,然后开始发送offer
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onJoinHome(myId, connections, roomSize);
+            }
+        });
+
+    }
+
+    @Override
+    public void onNewPeer(String userId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.newPeer(userId);
+            }
+        });
+
+    }
+
+    @Override
+    public void onReject(String userId, int type) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onRefuse(userId, type);
+            }
+        });
+
+    }
+
+    @Override
+    public void onOffer(String userId, String sdp) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onReceiveOffer(userId, sdp);
+            }
+        });
+
+
+    }
+
+    @Override
+    public void onAnswer(String userId, String sdp) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onReceiverAnswer(userId, sdp);
+            }
+        });
+
+    }
+
+    @Override
+    public void onIceCandidate(String userId, String id, int label, String candidate) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onRemoteIceCandidate(userId, id, label, candidate);
+            }
+        });
+
+    }
+
+    @Override
+    public void onLeave(String userId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onLeave(userId);
+            }
+        });
+    }
+
+    @Override
+    public void logout(String str) {
+        Log.i(TAG, "logout:" + str);
+        userState = 0;
+        if (iUserState != null && iUserState.get() != null) {
+            iUserState.get().userLogout();
+        }
+    }
+
+    @Override
+    public void onTransAudio(String userId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onTransAudio(userId);
+            }
+        });
+    }
+
+    @Override
+    public void onDisConnect(String userId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onDisConnect(userId, EnumType.CallEndReason.RemoteSignalError);
+            }
+        });
+    }
+
+    public boolean socketOpen() {
+        if (webSocket != null) {
+            return webSocket.isOpen();
+        }
+        return false;
+    }
+
+    @Override
+    public void reConnect() {
+        handler.post(() -> {
+            if (webSocket != null) {
+                webSocket.reconnect();
+            }
+        });
+    }
+    //===========================================================================================
+
+
+    public int getUserState() {
+        return userState;
+    }
+
+    private WeakReference<IUserState> iUserState;
+
+    public void addUserStateCallback(IUserState userState) {
+        iUserState = new WeakReference<>(userState);
+    }
+
+}

+ 35 - 0
WebRTC/src/main/java/com/wdkl/core/ui/event/MsgEvent.java

@@ -0,0 +1,35 @@
+package com.wdkl.core.ui.event;
+
+public class MsgEvent<T> {
+    private int code;
+    private T data;
+    public static final int CODE_ON_CALL_ENDED = 0X01;//语音视频通话结束
+    public static final int CODE_ON_REMOTE_RING = 0X02;//对方已响铃
+
+
+    public MsgEvent(int code) {
+        this.code = code;
+    }
+
+    public MsgEvent(int code, T data) {
+        this.code = code;
+        this.data = data;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+}
+

+ 42 - 0
WebRTC/src/main/java/com/wdkl/core/ui/user/UserBean.java

@@ -0,0 +1,42 @@
+package com.wdkl.core.ui.user;
+
+import android.text.TextUtils;
+
+/**
+ * Created by dds on 2020/4/13.
+ * android_shuai@163.com
+ */
+public class UserBean {
+    private String userId;
+    private String avatar;
+    private String nickName;
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getAvatar() {
+        return avatar;
+    }
+
+    public void setAvatar(String avatar) {
+        this.avatar = avatar;
+    }
+
+    public String getNickName() {
+        if (TextUtils.isEmpty(nickName)) {
+            return userId;
+        }
+        return nickName;
+    }
+
+    public void setNickName(String nickName) {
+        this.nickName = nickName;
+    }
+
+
+}

+ 53 - 0
WebRTC/src/main/java/com/wdkl/core/ui/user/UserListViewModel.java

@@ -0,0 +1,53 @@
+package com.wdkl.core.ui.user;
+
+import android.util.Log;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.alibaba.fastjson.JSON;
+import com.wdkl.core.consts.Urls;
+import com.wdkl.net.HttpRequestPresenter;
+import com.wdkl.net.ICallback;
+
+import java.util.List;
+
+public class UserListViewModel extends ViewModel {
+
+    private MutableLiveData<List<UserBean>> mList;
+
+    public LiveData<List<UserBean>> getUserList() {
+        if (mList == null) {
+            mList = new MutableLiveData<>();
+            loadUsers();
+        }
+        return mList;
+    }
+
+
+    // 获取远程用户列表
+    public void loadUsers() {
+        Thread thread = new Thread(() -> {
+            String url = Urls.getUserList();
+            HttpRequestPresenter.getInstance()
+                    .get(url, null, new ICallback() {
+                        @Override
+                        public void onSuccess(String result) {
+                            Log.d("dds_test", result);
+                            List<UserBean> userBeans = JSON.parseArray(result, UserBean.class);
+                            mList.postValue(userBeans);
+                        }
+
+                        @Override
+                        public void onFailure(int code, Throwable t) {
+                            Log.d("dds_test", "code:" + code + ",msg:" + t.toString());
+                        }
+                    });
+        });
+        thread.start();
+
+
+    }
+
+}

+ 131 - 0
WebRTC/src/main/java/com/wdkl/core/util/ActivityStackManager.java

@@ -0,0 +1,131 @@
+package com.wdkl.core.util;
+
+import android.app.Activity;
+import android.app.Application;
+import android.util.Log;
+
+import androidx.collection.ArrayMap;
+
+/**
+ * 应用程序Activity管理类,用于Activity管理和应用程序退出
+ *
+ * @author gong
+ */
+public class ActivityStackManager {
+    private static final String TAG = "ActivityStackManager";
+    private static volatile ActivityStackManager sInstance;
+
+    private final ArrayMap<String, Activity> mActivitySet = new ArrayMap<>();
+
+    /**
+     * 当前 Activity 对象标记
+     */
+    private String mCurrentTag;
+
+    private ActivityStackManager() {
+    }
+
+    public static ActivityStackManager getInstance() {
+        // 加入双重校验锁
+        if (sInstance == null) {
+            synchronized (ActivityStackManager.class) {
+                if (sInstance == null) {
+                    sInstance = new ActivityStackManager();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * 获取 Application 对象
+     */
+    public Application getApplication() {
+        return getTopActivity().getApplication();
+    }
+
+    /**
+     * 获取栈顶的 Activity
+     */
+    public Activity getTopActivity() {
+        return mActivitySet.get(mCurrentTag);
+    }
+
+    /**
+     * 销毁所有的 Activity
+     */
+    public void finishAllActivities() {
+        finishAllActivities((Class<? extends Activity>) null);
+    }
+
+
+    /**
+     * 获取栈底部的Activity
+     */
+    public Activity getBottomActivity() {
+        Log.d(TAG, "getBottomActivity mActivitySet.size() = " + mActivitySet.size());
+        if (mActivitySet.size() > 0) {
+            return mActivitySet.get(mActivitySet.keyAt(0));
+        } else {
+            return getTopActivity();
+        }
+
+    }
+    /**
+     * 销毁所有的 Activity,除这些 Class 之外的 Activity
+     */
+    @SafeVarargs
+    public final void finishAllActivities(Class<? extends Activity>... classArray) {
+        String[] keys = mActivitySet.keySet().toArray(new String[]{});
+        for (String key : keys) {
+            Activity activity = mActivitySet.get(key);
+            if (activity != null && !activity.isFinishing()) {
+                boolean whiteClazz = false;
+                if (classArray != null) {
+                    for (Class<? extends Activity> clazz : classArray) {
+                        if (activity.getClass() == clazz) {
+                            whiteClazz = true;
+                        }
+                    }
+                }
+                // 如果不是白名单上面的 Activity 就销毁掉
+                if (!whiteClazz) {
+                    activity.finish();
+                    mActivitySet.remove(key);
+                }
+            }
+        }
+    }
+
+    /**
+     * Activity 同名方法回调
+     */
+    public void onCreated(Activity activity) {
+        mCurrentTag = getObjectTag(activity);
+        mActivitySet.put(getObjectTag(activity), activity);
+    }
+
+    /**
+     * Activity 同名方法回调
+     */
+    public void onDestroyed(Activity activity) {
+        mActivitySet.remove(getObjectTag(activity));
+        // 如果当前的 Activity 是最后一个的话
+        if (getObjectTag(activity).equals(mCurrentTag)) {
+            // 清除当前标记
+            mCurrentTag = null;
+        }
+        if (mActivitySet.size() != 0) {
+            mCurrentTag = mActivitySet.keyAt(mActivitySet.size() - 1);
+        }
+    }
+
+    /**
+     * 获取一个对象的独立无二的标记
+     */
+    private static String getObjectTag(Object object) {
+        // 对象所在的包名 + 对象的内存地址
+        return object.getClass().getName() + Integer.toHexString(object.hashCode());
+    }
+
+}

+ 58 - 0
WebRTC/src/main/java/com/wdkl/core/util/CrashHandler.java

@@ -0,0 +1,58 @@
+package com.wdkl.core.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.SkyEngineKit;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+
+public class CrashHandler implements Thread.UncaughtExceptionHandler {
+    private static final String TAG = "MyUncaughtExceptionHand";
+
+    private Context appContext;
+
+    public CrashHandler(Context context) {
+        appContext = context;
+    }
+
+    @Override
+    public void uncaughtException(@NotNull Thread thread, @NotNull Throwable ex) {
+        SkyEngineKit gEngineKit = SkyEngineKit.Instance();
+        CallSession session = gEngineKit.getCurrentSession();
+
+        Log.d(TAG, "uncaughtException session = " + session);
+        if (session != null) {
+            gEngineKit.endCall();
+        } else {
+            //gEngineKit.sendDisconnected(App.getInstance().getRoomId(), App.getInstance().getOtherUserId(),true);
+        }
+        final Writer result = new StringWriter();
+        final PrintWriter printWriter = new PrintWriter(result);
+        //如果异常时在AsyncTask里面的后台线程抛出的
+        //那么实际的异常仍然可以通过getCause获得
+        Throwable cause = ex;
+        while (null != cause) {
+            cause.printStackTrace(printWriter);
+            cause = cause.getCause();
+        }
+        //stacktraceAsString就是获取的carsh堆栈信息
+        final String stacktraceAsString = result.toString();
+        printWriter.close();
+        restartApp();
+    }
+
+    private void restartApp() {
+        /*Intent i = new Intent(appContext, LauncherActivity.class);
+        i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+        appContext.startActivity(i);*/
+    }
+
+
+}

+ 127 - 0
WebRTC/src/main/java/com/wdkl/core/util/OSUtils.java

@@ -0,0 +1,127 @@
+package com.wdkl.core.util;
+
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Created by rhm on 2018/1/12.
+ */
+
+public class OSUtils {
+
+private static final String TAG = "Rom";
+
+    public static final String ROM_MIUI = "MIUI";
+    public static final String ROM_EMUI = "EMUI";
+    public static final String ROM_FLYME = "FLYME";
+    public static final String ROM_OPPO = "OPPO";
+    public static final String ROM_SMARTISAN = "SMARTISAN";
+    public static final String ROM_VIVO = "VIVO";
+    public static final String ROM_QIKU = "QIKU";
+
+    private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name";
+    private static final String KEY_VERSION_EMUI = "ro.build.version.emui";
+    private static final String KEY_VERSION_OPPO = "ro.build.version.opporom";
+    private static final String KEY_VERSION_SMARTISAN = "ro.smartisan.version";
+    private static final String KEY_VERSION_VIVO = "ro.vivo.os.version";
+
+    private static String sName;
+    private static String sVersion;
+
+    public static boolean isEmui() {
+        return check(ROM_EMUI);
+    }
+
+    public static boolean isMiui() {
+        return check(ROM_MIUI);
+    }
+
+    public static boolean isVivo() {
+        return check(ROM_VIVO);
+    }
+
+    public static boolean isOppo() {
+        return check(ROM_OPPO);
+    }
+
+    public static boolean isFlyme() {
+        return check(ROM_FLYME);
+    }
+
+    public static boolean is360() {
+        return check(ROM_QIKU) || check("360");
+    }
+
+    public static boolean isSmartisan() {
+        return check(ROM_SMARTISAN);
+    }
+
+    public static String getName() {
+        if (sName == null) {
+            check("");
+        }
+        return sName;
+    }
+
+    public static String getVersion() {
+        if (sVersion == null) {
+            check("");
+        }
+        return sVersion;
+    }
+
+    public static boolean check(String rom) {
+        if (sName != null) {
+            return sName.equals(rom);
+        }
+
+        if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_MIUI))) {
+            sName = ROM_MIUI;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_EMUI))) {
+            sName = ROM_EMUI;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_OPPO))) {
+            sName = ROM_OPPO;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_VIVO))) {
+            sName = ROM_VIVO;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_SMARTISAN))) {
+            sName = ROM_SMARTISAN;
+        } else {
+            sVersion = Build.DISPLAY;
+            if (sVersion.toUpperCase().contains(ROM_FLYME)) {
+                sName = ROM_FLYME;
+            } else {
+                sVersion = Build.UNKNOWN;
+                sName = Build.MANUFACTURER.toUpperCase();
+            }
+        }
+        return sName.equals(rom);
+    }
+
+    public static String getProp(String name) {
+        String line = null;
+        BufferedReader input = null;
+        try {
+            Process p = Runtime.getRuntime().exec("getprop " + name);
+            input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
+            line = input.readLine();
+            input.close();
+        } catch (IOException ex) {
+            Log.e(TAG, "Unable to read prop " + name, ex);
+            return null;
+        } finally {
+            if (input != null) {
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return line;
+    }
+}

+ 25 - 0
WebRTC/src/main/java/com/wdkl/core/util/StringUtil.java

@@ -0,0 +1,25 @@
+package com.wdkl.core.util;
+
+import java.util.List;
+
+/**
+ * Created by dds on 2020/5/4.
+ * ddssingsong@163.com
+ */
+public class StringUtil {
+
+    public static String listToString(List<String> mList) {
+        final String SEPARATOR = ",";
+        StringBuilder sb = new StringBuilder();
+        String convertedListStr;
+        if (null != mList && mList.size() > 0) {
+            for (String item : mList) {
+                sb.append(item);
+                sb.append(SEPARATOR);
+            }
+            convertedListStr = sb.toString();
+            convertedListStr = convertedListStr.substring(0, convertedListStr.length() - SEPARATOR.length());
+            return convertedListStr;
+        } else return "";
+    }
+}

+ 52 - 0
WebRTC/src/main/java/com/wdkl/core/util/Utils.java

@@ -0,0 +1,52 @@
+package com.wdkl.core.util;
+
+import android.app.ActivityManager;
+import android.app.Application;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.os.Build;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import java.util.List;
+
+public class Utils {
+    //设置界面全屏
+    public static void setFullScreenWindowLayout(Window window) {
+        window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.
+                SYSTEM_UI_FLAG_LAYOUT_STABLE);
+        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            window.setStatusBarColor(Color.TRANSPARENT);
+        }
+        //设置页面全屏显示
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            WindowManager.LayoutParams layoutParams = window.getAttributes();
+            layoutParams.layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+            window.setAttributes(layoutParams);
+        }
+    }
+
+    public static int getStatusBarHeight(Context context) {
+        Resources resources = context.getResources();
+        int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
+        return resources.getDimensionPixelSize(resourceId);
+    }
+
+
+    public static boolean isAppRunningForeground(Context context) {
+        ActivityManager activityManager =
+                (ActivityManager) context.getSystemService(Application.ACTIVITY_SERVICE);
+        List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = activityManager.getRunningAppProcesses();
+        for (ActivityManager.RunningAppProcessInfo appProcessInfo : runningAppProcesses) {
+            if (appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+                if (appProcessInfo.processName.equals(context.getApplicationInfo().processName))
+                    return true;
+            }
+        }
+        return false;
+    }
+}

+ 192 - 0
WebRTC/src/main/java/com/wdkl/core/voip/AsyncPlayer.java

@@ -0,0 +1,192 @@
+package com.wdkl.core.voip;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.LinkedList;
+
+/**
+ * 响铃相关类
+ */
+public class AsyncPlayer {
+    private static final int PLAY = 1;
+    private static final int STOP = 2;
+    private AudioManager audioManager;
+
+    private static final class Command {
+        int code;
+        Context context;
+        int resId;
+        boolean looping;
+        int stream;
+        long requestTime;
+
+        public String toString() {
+            return "{ code=" + code + " looping=" + looping + " stream=" + stream + " resId=" + resId + " }";
+        }
+    }
+
+    private final LinkedList mCmdQueue = new LinkedList();
+
+    private void startSound(Command cmd) {
+
+        try {
+            //MediaPlayer player = new MediaPlayer();
+            MediaPlayer player = MediaPlayer.create(cmd.context, cmd.resId);
+            player.setAudioStreamType(cmd.stream);
+            //player.setDataSource(cmd.context, cmd.uri);
+            player.setLooping(cmd.looping);
+            player.setVolume(1.0f, 1.0f);
+            //player.prepare();
+            player.start();
+            if (mPlayer != null) {
+                mPlayer.release();
+            }
+            mPlayer = player;
+            Log.w(mTag, "start sound " + cmd.resId);
+        } catch (Exception e) {
+            Log.w(mTag, "error loading sound for " + cmd.resId, e);
+        }
+    }
+
+    private final class PlayThread extends java.lang.Thread {
+        PlayThread() {
+            super("AsyncPlayer-" + mTag);
+        }
+
+        public void run() {
+            while (true) {
+                Command cmd = null;
+
+                synchronized (mCmdQueue) {
+
+                    cmd = (Command) mCmdQueue.removeFirst();
+                }
+
+                switch (cmd.code) {
+                    case PLAY:
+                        startSound(cmd);
+                        break;
+                    case STOP:
+                        if (mPlayer != null) {
+                            mPlayer.stop();
+                            mPlayer.reset();
+                            mPlayer.release();
+                            mPlayer = null;
+                        } else {
+                            Log.w(mTag, "STOP command without a player");
+                        }
+                        break;
+                }
+
+                synchronized (mCmdQueue) {
+                    if (mCmdQueue.size() == 0) {
+
+                        mThread = null;
+                        releaseWakeLock();
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
+    private String mTag;
+    private Thread mThread;
+    private MediaPlayer mPlayer;
+    private PowerManager.WakeLock mWakeLock;
+
+    private int mState = STOP;
+
+    public AsyncPlayer(String tag) {
+        if (tag != null) {
+            mTag = tag;
+        } else {
+            mTag = "AsyncPlayer";
+        }
+    }
+
+    public void play(Context context, int res, boolean looping, int stream) {
+        Command cmd = new Command();
+        cmd.requestTime = SystemClock.uptimeMillis();
+        cmd.code = PLAY;
+        cmd.context = context;
+        cmd.resId = res;
+        cmd.looping = looping;
+        cmd.stream = stream;
+        synchronized (mCmdQueue) {
+            enqueueLocked(cmd);
+            mState = PLAY;
+        }
+    }
+
+    public void stop() {
+        synchronized (mCmdQueue) {
+            if (mState != STOP) {
+                Command cmd = new Command();
+                cmd.requestTime = SystemClock.uptimeMillis();
+                cmd.code = STOP;
+                enqueueLocked(cmd);
+                mState = STOP;
+            }
+        }
+    }
+
+    public boolean isPlay() {
+        return mState == PLAY;
+    }
+
+    private void enqueueLocked(Command cmd) {
+        mCmdQueue.add(cmd);
+        if (mThread == null) {
+            acquireWakeLock();
+            mThread = new PlayThread();
+            mThread.start();
+        }
+    }
+
+    public void setUsesWakeLock(Context context) {
+        if (mWakeLock != null || mThread != null) {
+            throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock + " mThread=" + mThread);
+        }
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
+    }
+
+    private void acquireWakeLock() {
+        if (mWakeLock != null) {
+            mWakeLock.acquire();
+        }
+    }
+
+    private void releaseWakeLock() {
+        if (mWakeLock != null) {
+            mWakeLock.release();
+        }
+    }
+
+    private boolean isHeadphonesPlugged(Context context) {
+        if (audioManager == null) {
+            audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        }
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+            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();
+        }
+    }
+}

+ 129 - 0
WebRTC/src/main/java/com/wdkl/core/voip/CallForegroundNotification.java

@@ -0,0 +1,129 @@
+package com.wdkl.core.voip;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+
+import com.wdkl.core.util.ActivityStackManager;
+import com.wdkl.webrtc.R;
+
+import java.util.Random;
+
+/**
+ * <pre>
+ *     author : Jasper
+ *     e-mail : 229605030@qq.com
+ *     time   : 2021/02/01
+ *     desc   :
+ * </pre>
+ */
+public class CallForegroundNotification extends ContextWrapper {
+    private static final String TAG = "CallForegroundNotificat";
+    private static final String id = "channel1";
+    private static final String name = "voip";
+    private NotificationManager manager;
+
+    public CallForegroundNotification(Context base) {
+        super(base);
+        manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+    }
+
+    @RequiresApi(api = 26)
+    public void createNotificationChannel() {
+        NotificationChannel channel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH);
+        channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
+        manager.createNotificationChannel(channel);
+    }
+
+    public void sendRequestIncomingPermissionsNotification(
+            Context context, String room, String userList, String inviteId, String inviteUserName, Boolean isAudioOnly
+    ) {
+        clearAllNotification();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            createNotificationChannel();
+        }
+        //发送广播,调起接听界面
+        Intent intent = new Intent(context, ActivityStackManager.getInstance().getBottomActivity().getClass()); //栈底是MainActivity
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra("room", room);
+        intent.putExtra("isFromCall", true);
+        intent.putExtra("audioOnly", isAudioOnly);
+        intent.putExtra("inviteUserName", inviteUserName);
+        intent.putExtra("inviteId", inviteId);
+        intent.putExtra("userList", userList);
+        PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, new Random().nextInt(100), intent, PendingIntent.FLAG_UPDATE_CURRENT);
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, id)
+                .setSmallIcon(R.mipmap.ic_launcher)
+                .setContentTitle(getResources().getString(R.string.app_name))
+                .setTicker(
+                        "您收到" + inviteUserName + "的来电邀请,请允许"
+                                + (isAudioOnly ? "录音" :
+                                "录音和相机") + "权限来通话"
+                )
+                .setContentText("您收到" + inviteUserName + "的来电邀请,请允许"
+                        + (isAudioOnly ? "录音" :
+                        "录音和相机") + "权限来通话"
+                )
+                .setAutoCancel(true)
+                .setDefaults(Notification.DEFAULT_ALL)
+                .setPriority(NotificationCompat.PRIORITY_MAX)
+                .setCategory(Notification.CATEGORY_CALL);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            notificationBuilder.setFullScreenIntent(fullScreenPendingIntent, true);
+        } else {
+            notificationBuilder.setContentIntent(fullScreenPendingIntent);
+        }
+//
+        manager.notify(10086, notificationBuilder.build());
+    }
+
+    public void sendIncomingCallNotification(
+            Context context, String targetId, Boolean isOutgoing, String inviteUserName,
+            Boolean isAudioOnly, Boolean isClearTop
+    ) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            clearAllNotification();
+            createNotificationChannel();
+            Notification notification = getChannelNotificationQ(context, targetId, isOutgoing, inviteUserName, isAudioOnly, isClearTop);
+            manager.notify(10086, notification);
+        }
+    }
+
+    private void clearAllNotification() {
+        manager.cancelAll();
+    }
+
+    private Notification getChannelNotificationQ(
+            Context context, String targetId, Boolean isOutgoing, String inviteUserName,
+            Boolean isAudioOnly, Boolean isClearTop
+    ) {
+        Intent fullScreenIntent = CallSingleActivity.getCallIntent(context, targetId, isOutgoing, inviteUserName, isAudioOnly, isClearTop);
+        fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, new Random().nextInt(100), fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, id)
+                .setSmallIcon(R.mipmap.ic_launcher)
+                .setContentTitle(getResources().getString(R.string.app_name))
+                .setTicker(inviteUserName + "来电")
+                .setContentText(inviteUserName + "来电")
+                .setAutoCancel(true)
+                .setDefaults(Notification.DEFAULT_ALL)
+                .setPriority(NotificationCompat.PRIORITY_MAX)
+                .setCategory(Notification.CATEGORY_CALL)
+                .setFullScreenIntent(fullScreenPendingIntent, true);
+
+        Log.d(TAG, "getChannelNotificationQ");
+        return notificationBuilder.build();
+    }
+
+
+}

+ 248 - 0
WebRTC/src/main/java/com/wdkl/core/voip/CallMultiActivity.java

@@ -0,0 +1,248 @@
+package com.wdkl.core.voip;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.wdkl.core.base.BaseActivity;
+import com.wdkl.ncs.android.component.nursehome.common.Constants;
+import com.wdkl.ncs.android.middleware.tcp.channel.DeviceChannel;
+import com.wdkl.ncs.android.middleware.tcp.channel.VideoUtil;
+import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel;
+import com.wdkl.ncs.android.middleware.tcp.enums.TcpAction;
+import com.wdkl.ncs.android.middleware.tcp.enums.TcpType;
+import com.wdkl.ncs.android.middleware.utils.MessageEvent;
+import com.wdkl.permission.Permissions;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.skywebrtc.except.NotInitializedException;
+import com.wdkl.webrtc.R;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.UUID;
+
+/**
+ * Created by dds on 2018/7/26.
+ * 多人通话界面
+ */
+public class CallMultiActivity extends BaseActivity implements CallSession.CallSessionCallback, View.OnClickListener {
+    private SkyEngineKit gEngineKit;
+    private final Handler handler = new Handler(Looper.getMainLooper());
+    private ImageView meetingHangupImageView;
+    private CallSession.CallSessionCallback currentFragment;
+    public static final String EXTRA_MO = "isOutGoing";
+    private boolean isOutgoing;
+
+    private boolean leave;
+
+    public static void openActivity(Activity activity, String room, boolean isOutgoing) {
+        Intent intent = new Intent(activity, CallMultiActivity.class);
+        intent.putExtra("room", room);
+        intent.putExtra(EXTRA_MO, isOutgoing);
+        activity.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN |
+                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
+                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
+                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+        getWindow().getDecorView().setSystemUiVisibility(getSystemUiVisibility());
+
+        if (!EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().register(this);
+        }
+
+        setContentView(R.layout.activity_multi_call);
+        initView();
+        initListener();
+        initData();
+    }
+
+
+    private void initView() {
+        meetingHangupImageView = findViewById(R.id.meetingHangupImageView);
+        Fragment fragment = new FragmentMeeting();
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        fragmentManager.beginTransaction()
+                .add(R.id.meeting_container, fragment)
+                .commit();
+        currentFragment = (CallSession.CallSessionCallback) fragment;
+    }
+
+    private void initListener() {
+        meetingHangupImageView.setOnClickListener(this);
+    }
+
+    private void initData() {
+        Intent intent = getIntent();
+        String room = intent.getStringExtra("room");
+        isOutgoing = intent.getBooleanExtra(EXTRA_MO, false);
+        try {
+            gEngineKit = SkyEngineKit.Instance();
+        } catch (NotInitializedException e) {
+            finish();
+        }
+        String[] per = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+        Permissions.request(this, per, integer -> {
+            if (integer == 0) {
+                // 权限同意
+                init(room, isOutgoing);
+            } else {
+                // 权限拒绝
+                CallMultiActivity.this.finish();
+            }
+        });
+
+
+    }
+
+    private void init(String room, boolean isOutgoing) {
+        SkyEngineKit.init(new VoipEvent());
+        if (isOutgoing) {
+            // 创建一个房间并进入
+            gEngineKit.createAndJoinRoom(this, room);
+        } else {
+            // 加入房间
+            gEngineKit.joinRoom(this, room);
+        }
+
+
+        CallSession session = gEngineKit.getCurrentSession();
+        if (session == null) {
+            this.finish();
+        } else {
+            session.setSessionCallback(this);
+            session.toggleSpeaker(true);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        DeviceChannel.calling = false;
+        if (EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().unregister(this);
+        }
+    }
+
+    public SkyEngineKit getEngineKit() {
+        return gEngineKit;
+    }
+
+
+    @TargetApi(19)
+    private static int getSystemUiVisibility() {
+        int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+        }
+        return flags;
+    }
+
+
+    //-------------------------------------------------回调相关------------------------------------
+    @Override
+    public void didCallEndWithReason(EnumType.CallEndReason var1) {
+        finish();
+    }
+
+    @Override
+    public void didChangeState(EnumType.CallState callState) {
+        handler.post(() -> currentFragment.didChangeState(callState));
+    }
+
+    @Override
+    public void didChangeMode(boolean var1) {
+        handler.post(() -> currentFragment.didChangeMode(var1));
+    }
+
+    @Override
+    public void didCreateLocalVideoTrack() {
+        handler.post(() -> currentFragment.didCreateLocalVideoTrack());
+    }
+
+    @Override
+    public void didReceiveRemoteVideoTrack(String userId) {
+        handler.post(() -> currentFragment.didReceiveRemoteVideoTrack(userId));
+    }
+
+    @Override
+    public void didUserLeave(String userId) {
+        //handler.post(() -> currentFragment.didUserLeave(userId));
+
+        if (!leave) {
+            leave = true;
+            DeviceChannel.calling = false;
+            SkyEngineKit.Instance().leaveRoom();
+        }
+        finish();
+    }
+
+    @Override
+    public void didError(String var1) {
+        finish();
+    }
+
+    @Override
+    public void didDisconnected(String userId) {
+
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v.getId() == R.id.meetingHangupImageView) {
+            handleHangup();
+        }
+
+        /*switch (v.getId()) {
+            case R.id.meetingHangupImageView:
+                handleHangup();
+                break;
+        }*/
+
+    }
+
+    // 处理挂断事件
+    private void handleHangup() {
+        if (!leave) {
+            leave = true;
+            DeviceChannel.calling = false;
+            VideoUtil.handoffVideoCall(Constants.Companion.getDeviceId(), Constants.Companion.getFromId(), Constants.Companion.getInteractionId());
+            SkyEngineKit.Instance().leaveRoom();
+        }
+    }
+
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    public void onMoonEvent(MessageEvent messageEvent) {
+        if (messageEvent.getType() == Constants.VIDEO_MSG){
+            if (messageEvent.getMessage() instanceof TcpModel) {
+                TcpModel tcpModel = (TcpModel) messageEvent.getMessage();
+                if (tcpModel.getAction() == TcpAction.VideoAction.HANDOFF && !leave) {
+                    leave = true;
+                    DeviceChannel.calling = false;
+                    SkyEngineKit.Instance().leaveRoom();
+                }
+            }
+        }
+    }
+}

+ 394 - 0
WebRTC/src/main/java/com/wdkl/core/voip/CallSingleActivity.java

@@ -0,0 +1,394 @@
+package com.wdkl.core.voip;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v4.app.FragmentManager;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+
+import com.wdkl.core.base.BaseActivity;
+import com.wdkl.ncs.android.middleware.tcp.channel.VoiceUtil;
+import com.wdkl.permission.Permissions;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.skywebrtc.except.NotInitializedException;
+import com.wdkl.webrtc.R;
+
+import java.util.UUID;
+
+
+/**
+ * Created by dds on 2018/7/26.
+ * 单人通话界面
+ */
+public class CallSingleActivity extends BaseActivity implements CallSession.CallSessionCallback {
+
+    public static final String EXTRA_TARGET = "targetId";
+    public static final String EXTRA_MO = "isOutGoing";
+    public static final String EXTRA_AUDIO_ONLY = "audioOnly";
+    public static final String EXTRA_USER_NAME = "userName";
+    public static final String EXTRA_FROM_FLOATING_VIEW = "fromFloatingView";
+    private static final String TAG = "CallSingleActivity";
+
+    private Handler handler = new Handler(Looper.getMainLooper());
+    private boolean isOutgoing;
+    private String targetId;
+    boolean isAudioOnly;
+    private boolean isFromFloatingView;
+
+    private SkyEngineKit gEngineKit;
+
+    private SingleCallFragment currentFragment;
+    private String room;
+
+    public static Intent getCallIntent(Context context, String targetId, boolean isOutgoing, String inviteUserName,
+                                       boolean isAudioOnly, boolean isClearTop) {
+        Intent voip = new Intent(context, CallSingleActivity.class);
+        voip.putExtra(CallSingleActivity.EXTRA_MO, isOutgoing);
+        voip.putExtra(CallSingleActivity.EXTRA_TARGET, targetId);
+        voip.putExtra(CallSingleActivity.EXTRA_USER_NAME, inviteUserName);
+        voip.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, isAudioOnly);
+        voip.putExtra(CallSingleActivity.EXTRA_FROM_FLOATING_VIEW, false);
+        if (isClearTop) {
+            voip.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        }
+        return voip;
+    }
+
+
+    public static void openActivity(Context context, String targetId, boolean isOutgoing, String inviteUserName,
+                                    boolean isAudioOnly, boolean isClearTop) {
+        Intent intent = getCallIntent(context, targetId, isOutgoing, inviteUserName, isAudioOnly, isClearTop);
+        //if (context instanceof Activity) {
+        //    context.startActivity(intent);
+        //} else {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(intent);
+        //}
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setStatusBarOrScreenStatus(this);
+        setContentView(R.layout.activity_single_call);
+
+        try {
+            gEngineKit = SkyEngineKit.Instance();
+        } catch (NotInitializedException e) {
+            SkyEngineKit.init(new VoipEvent()); //重新初始化
+            try {
+                gEngineKit = SkyEngineKit.Instance();
+            } catch (NotInitializedException ex) {
+                finish();
+            }
+        }
+        final Intent intent = getIntent();
+        targetId = intent.getStringExtra(EXTRA_TARGET);
+        isFromFloatingView = intent.getBooleanExtra(EXTRA_FROM_FLOATING_VIEW, false);
+        isOutgoing = intent.getBooleanExtra(EXTRA_MO, false);
+        isAudioOnly = intent.getBooleanExtra(EXTRA_AUDIO_ONLY, false);
+
+        if (isFromFloatingView) {
+            //Intent serviceIntent = new Intent(this, FloatingVoipService.class);
+            //stopService(serviceIntent);
+            //init(targetId, false, isAudioOnly, false);
+        } else {
+            // 权限检测
+            String[] per;
+            if (isAudioOnly) {
+                per = new String[]{Manifest.permission.RECORD_AUDIO};
+            } else {
+                per = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+            }
+            Permissions.request(this, per, integer -> {
+                Log.d(TAG, "Permissions.request integer = " + integer);
+                if (integer == 0) {
+                    // 权限同意
+                    init(targetId, isOutgoing, isAudioOnly);
+                } else {
+                    Toast.makeText(this, "权限被拒绝", Toast.LENGTH_SHORT).show();
+                    // 权限拒绝
+                    finish();
+                }
+            });
+        }
+
+
+    }
+
+    @Override
+    public void onBackPressed() {
+        //通话时不能按返回键,跟微信同现象,只能挂断结束或者接听
+//        super.onBackPressed();
+//        if (currentFragment != null) {
+//            if (currentFragment instanceof FragmentAudio) {
+//                ((FragmentAudio) currentFragment).onBackPressed();
+//            } else if (currentFragment instanceof FragmentVideo) {
+//                ((FragmentVideo) currentFragment).onBackPressed();
+//            }
+//        }
+
+    }
+
+    private void init(String targetId, boolean outgoing, boolean audioOnly) {
+        SingleCallFragment fragment;
+        if (audioOnly) {
+            fragment = new FragmentAudio();
+        } else {
+            fragment = new FragmentVideo();
+        }
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        currentFragment = fragment;
+        fragmentManager.beginTransaction()
+                    .replace(android.R.id.content, fragment)
+                    .commit();
+
+        if (outgoing) {
+            // 创建会话
+            room = UUID.randomUUID().toString() + System.currentTimeMillis();
+            boolean b = gEngineKit.startOutCall(getApplicationContext(), room, targetId, audioOnly);
+            if (!b) {
+                finish();
+                return;
+            }
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session == null) {
+                finish();
+            } else {
+                session.setSessionCallback(this);
+                session.toggleSpeaker(true);
+            }
+        } else {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session == null) {
+                finish();
+            } else {
+                /*if (session.isAudioOnly() && !audioOnly) { //这种情况是,对方切换成音频的时候,activity还没启动,这里启动后需要切换一下
+                    isAudioOnly = session.isAudioOnly();
+                    fragment.didChangeMode(true);
+                }*/
+                Log.d(TAG, "session = " + session + "; session.getState() = " + session.getState());
+                session.setSessionCallback(this);
+                session.toggleSpeaker(true);
+
+                //之前已经通过tcp接受了,此时sip通话建立,直接接通
+                if (session.getState() == EnumType.CallState.Incoming) {
+                    session.joinHome(session.getRoomId());
+                } else {
+                    session.sendRefuse();
+                }
+            }
+        }
+
+    }
+
+    private void init(String targetId, boolean outgoing, boolean audioOnly, boolean isReplace) {
+        SingleCallFragment fragment;
+        if (audioOnly) {
+            fragment = new FragmentAudio();
+        } else {
+            fragment = new FragmentVideo();
+        }
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        currentFragment = fragment;
+        if (isReplace) {
+            fragmentManager.beginTransaction()
+                    .replace(android.R.id.content, fragment)
+                    .commit();
+        } else {
+            fragmentManager.beginTransaction()
+                    .add(android.R.id.content, fragment)
+                    .commit();
+        }
+        if (outgoing && !isReplace) {
+            // 创建会话
+            room = UUID.randomUUID().toString() + System.currentTimeMillis();
+            boolean b = gEngineKit.startOutCall(getApplicationContext(), room, targetId, audioOnly);
+            if (!b) {
+                finish();
+                return;
+            }
+            //App.getInstance().setRoomId(room);
+            //App.getInstance().setOtherUserId(targetId);
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session == null) {
+                finish();
+            } else {
+                session.setSessionCallback(this);
+            }
+        } else {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session == null) {
+                finish();
+            } else {
+                //if (session.isAudioOnly() && !audioOnly) { //这种情况是,对方切换成音频的时候,activity还没启动,这里启动后需要切换一下
+                //    isAudioOnly = session.isAudioOnly();
+                //    fragment.didChangeMode(true);
+                //}
+                session.setSessionCallback(this);
+            }
+        }
+
+    }
+
+    public SkyEngineKit getEngineKit() {
+        return gEngineKit;
+    }
+
+    public boolean isOutgoing() {
+        return isOutgoing;
+    }
+
+
+    public boolean isFromFloatingView() {
+        return isFromFloatingView;
+    }
+
+    // 显示小窗
+    /*public void showFloatingView() {
+        if (!checkOverlayPermission()) {
+            return;
+        }
+        Intent intent = new Intent(this, FloatingVoipService.class);
+        intent.putExtra(EXTRA_TARGET, targetId);
+        intent.putExtra(EXTRA_AUDIO_ONLY, isAudioOnly);
+        intent.putExtra(EXTRA_MO, isOutgoing);
+        startService(intent);
+        finish();
+    }*/
+
+    // 切换到语音通话
+    public void switchAudio() {
+        //init(targetId, isOutgoing, true, true);
+    }
+
+    public String getRoomId() {
+        return room;
+    }
+
+    // ======================================界面回调================================
+    @Override
+    public void didCallEndWithReason(EnumType.CallEndReason reason) {
+        //App.getInstance().setOtherUserId("0");
+        //交给fragment去finish
+//        finish();
+        handler.post(() -> currentFragment.didCallEndWithReason(reason));
+    }
+
+    @Override
+    public void didChangeState(EnumType.CallState callState) {
+        if (callState == EnumType.CallState.Connected) {
+            isOutgoing = false;
+        }
+        handler.post(() -> currentFragment.didChangeState(callState));
+    }
+
+    @Override
+    public void didChangeMode(boolean var1) {
+        handler.post(() -> currentFragment.didChangeMode(var1));
+    }
+
+    @Override
+    public void didCreateLocalVideoTrack() {
+        handler.post(() -> currentFragment.didCreateLocalVideoTrack());
+    }
+
+    @Override
+    public void didReceiveRemoteVideoTrack(String userId) {
+        handler.post(() -> currentFragment.didReceiveRemoteVideoTrack(userId));
+    }
+
+    @Override
+    public void didUserLeave(String userId) {
+        handler.post(() -> currentFragment.didUserLeave(userId));
+    }
+
+    @Override
+    public void didError(String var1) {
+        handler.post(() -> currentFragment.didError(var1));
+//        finish();
+    }
+
+    @Override
+    public void didDisconnected(String userId) {
+        handler.post(() -> currentFragment.didDisconnected(userId));
+    }
+
+
+    // ========================================================================================
+
+    private boolean checkOverlayPermission() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            SettingsCompat.setDrawOverlays(this, true);
+            if (!SettingsCompat.canDrawOverlays(this)) {
+                Toast.makeText(this, "需要悬浮窗权限", Toast.LENGTH_LONG).show();
+                SettingsCompat.manageDrawOverlays(this);
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    @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);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        //todo 自己下逻辑
+//        Constant.CALL_STATE = Constant.CALL_STANDBY;
+//        VoiceUtil.handoffAudioCall(Constant.DEVICE_ID, Constant.fromId, Constant.interactionId);
+    }
+}

+ 150 - 0
WebRTC/src/main/java/com/wdkl/core/voip/FragmentAudio.java

@@ -0,0 +1,150 @@
+package com.wdkl.core.voip;
+
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import com.blankj.utilcode.util.BarUtils;
+import com.wdkl.core.util.OSUtils;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType.CallState;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+
+/**
+ * Created by dds on 2018/7/26.
+ * android_shuai@163.com
+ * 语音通话控制界面
+ */
+public class FragmentAudio extends SingleCallFragment implements View.OnClickListener {
+    private static final String TAG = "FragmentAudio";
+    private ImageView muteImageView;
+    private ImageView speakerImageView;
+    private boolean micEnabled = false; // 静音
+    private boolean isSpeakerOn = false; // 扬声器
+
+    @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);
+        minimizeImageView.setOnClickListener(this);
+        muteImageView.setOnClickListener(this);
+        acceptImageView.setOnClickListener(this);
+        speakerImageView.setOnClickListener(this);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || OSUtils.isMiui() || OSUtils.isFlyme()) {
+            lytParent.post(() -> {
+                RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) minimizeImageView.getLayoutParams();
+                params.topMargin = BarUtils.getStatusBarHeight();
+                minimizeImageView.setLayoutParams(params);
+            });
+        }
+    }
+
+    @Override
+    public void init() {
+        super.init();
+        CallSession currentSession = gEngineKit.getCurrentSession();
+        currentState = currentSession.getState();
+        // 如果已经接通
+        if (currentSession != null && currentState == CallState.Connected) {
+            descTextView.setVisibility(View.GONE); // 提示语
+            outgoingActionContainer.setVisibility(View.VISIBLE);
+            durationTextView.setVisibility(View.VISIBLE);
+            //minimizeImageView.setVisibility(View.VISIBLE);
+            startRefreshTime();
+        } else {
+            // 如果未接通
+            if (isOutgoing) {
+                descTextView.setText(R.string.av_waiting);
+                outgoingActionContainer.setVisibility(View.VISIBLE);
+                incomingActionContainer.setVisibility(View.GONE);
+            } else {
+                descTextView.setText(R.string.av_audio_invite);
+                outgoingActionContainer.setVisibility(View.GONE);
+                incomingActionContainer.setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    public void didChangeState(CallState state) {
+
+        currentState = state;
+        runOnUiThread(() -> {
+            if (state == CallState.Connected) {
+                handler.removeMessages(WHAT_DELAY_END_CALL);
+                incomingActionContainer.setVisibility(View.GONE);
+                outgoingActionContainer.setVisibility(View.VISIBLE);
+                //minimizeImageView.setVisibility(View.VISIBLE);
+                descTextView.setVisibility(View.GONE);
+
+                startRefreshTime();
+            } else {
+                // do nothing now
+            }
+        });
+    }
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        // 接听
+        if (id == R.id.acceptImageView) {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session != null)
+                Log.d(TAG, "session = " + session + "; session.getState() = " + session.getState());
+            if (session != null && session.getState() == CallState.Incoming) {
+                session.joinHome(session.getRoomId());
+            } else if (session != null) {
+                session.sendRefuse();
+            }
+        }
+        // 挂断电话
+        if (id == R.id.incomingHangupImageView || id == R.id.outgoingHangupImageView) {
+            //App.getInstance().setOtherUserId("0");
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session != null) {
+                SkyEngineKit.Instance().endCall();
+            }
+            //            activity.finish();
+            //再onEvent中结束,防止ChatActivity结束了,消息发送不了
+        }
+        // 静音
+        if (id == R.id.muteImageView) {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session != null && session.getState() != CallState.Idle) {
+                if (session.toggleMuteAudio(!micEnabled)) {
+                    micEnabled = !micEnabled;
+                }
+                muteImageView.setSelected(micEnabled);
+            }
+        }
+        // 扬声器
+        if (id == R.id.speakerImageView) {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session != null && session.getState() != CallState.Idle) {
+                if (session.toggleSpeaker(!isSpeakerOn)) {
+                    isSpeakerOn = !isSpeakerOn;
+                }
+                speakerImageView.setSelected(isSpeakerOn);
+            }
+        }
+        // 小窗
+        /*if (id == R.id.minimizeImageView) {
+            if (callSingleActivity != null) {
+                callSingleActivity.showFloatingView();
+            }
+        }*/
+    }
+}

+ 118 - 0
WebRTC/src/main/java/com/wdkl/core/voip/FragmentMeeting.java

@@ -0,0 +1,118 @@
+package com.wdkl.core.voip;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+
+/**
+ * Created by dds on 2020/5/24.
+ * ddssingsong@163.com
+ */
+public class FragmentMeeting extends Fragment implements CallSession.CallSessionCallback, View.OnClickListener {
+    private SkyEngineKit gEngineKit;
+    private CallMultiActivity activity;
+    private NineGridView grid_view;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRetainInstance(true);
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        activity = (CallMultiActivity) getActivity();
+        if (activity != null) {
+
+        }
+
+    }
+
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_meeting, container, false);
+        initView(view);
+        init();
+        return view;
+    }
+
+    private void initView(View view) {
+        grid_view = view.findViewById(R.id.grid_view);
+
+    }
+
+
+    private void init() {
+        gEngineKit = activity.getEngineKit();
+    }
+
+    @Override
+    public void onClick(View v) {
+
+    }
+
+
+    @Override
+    public void didCallEndWithReason(EnumType.CallEndReason var1) {
+
+    }
+
+    @Override
+    public void didChangeState(EnumType.CallState var1) {
+
+    }
+
+    @Override
+    public void didChangeMode(boolean isAudioOnly) {
+
+    }
+
+    @Override
+    public void didCreateLocalVideoTrack() {
+        View surfaceView = gEngineKit.getCurrentSession().setupLocalVideo(true);
+        if (surfaceView != null) {
+            CallSession callSession = SkyEngineKit.Instance().getCurrentSession();
+            grid_view.addView(callSession.mMyId, surfaceView);
+
+
+        }
+
+    }
+
+    @Override
+    public void didReceiveRemoteVideoTrack(String userId) {
+        View surfaceView = gEngineKit.getCurrentSession().setupRemoteVideo(userId, true);
+        if (surfaceView != null) {
+            grid_view.addView(userId, surfaceView);
+        }
+    }
+
+    @Override
+    public void didUserLeave(String userId) {
+        //grid_view.removeView(userId);
+    }
+
+    @Override
+    public void didError(String error) {
+
+    }
+
+    @Override
+    public void didDisconnected(String userId) {
+
+    }
+
+
+}

+ 317 - 0
WebRTC/src/main/java/com/wdkl/core/voip/FragmentVideo.java

@@ -0,0 +1,317 @@
+package com.wdkl.core.voip;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+
+import com.blankj.utilcode.util.BarUtils;
+import com.wdkl.core.util.OSUtils;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType.CallState;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+
+import org.webrtc.SurfaceViewRenderer;
+
+/**
+ * Created by dds on 2018/7/26.
+ * android_shuai@163.com
+ * 视频通话控制界面
+ */
+public class FragmentVideo extends SingleCallFragment implements View.OnClickListener {
+    private static final String TAG = "FragmentVideo";
+    private ImageView outgoingAudioOnlyImageView;
+    private LinearLayout audioLayout;
+    private ImageView incomingAudioOnlyImageView;
+    private LinearLayout hangupLinearLayout;
+    private LinearLayout acceptLinearLayout;
+    private ImageView connectedAudioOnlyImageView;
+    private ImageView connectedHangupImageView;
+    private ImageView switchCameraImageView;
+    private FrameLayout fullscreenRenderer;
+    private FrameLayout pipRenderer;
+    private LinearLayout inviteeInfoContainer;
+    private boolean isFromFloatingView = false;
+    private SurfaceViewRenderer localSurfaceView;
+    private SurfaceViewRenderer remoteSurfaceView;
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        if (callSingleActivity != null) {
+            isFromFloatingView = callSingleActivity.isFromFloatingView();
+        }
+    }
+
+    @Override
+    int getLayout() {
+        return R.layout.fragment_video;
+    }
+
+    @Override
+    public void initView(View view) {
+        super.initView(view);
+        fullscreenRenderer = view.findViewById(R.id.fullscreen_video_view);
+        pipRenderer = view.findViewById(R.id.pip_video_view);
+        inviteeInfoContainer = view.findViewById(R.id.inviteeInfoContainer);
+        outgoingAudioOnlyImageView = view.findViewById(R.id.outgoingAudioOnlyImageView);
+        audioLayout = view.findViewById(R.id.audioLayout);
+        incomingAudioOnlyImageView = view.findViewById(R.id.incomingAudioOnlyImageView);
+        hangupLinearLayout = view.findViewById(R.id.hangupLinearLayout);
+        acceptLinearLayout = view.findViewById(R.id.acceptLinearLayout);
+        connectedAudioOnlyImageView = view.findViewById(R.id.connectedAudioOnlyImageView);
+        connectedHangupImageView = view.findViewById(R.id.connectedHangupImageView);
+        switchCameraImageView = view.findViewById(R.id.switchCameraImageView);
+        outgoingHangupImageView.setOnClickListener(this);
+        incomingHangupImageView.setOnClickListener(this);
+        minimizeImageView.setOnClickListener(this);
+        connectedHangupImageView.setOnClickListener(this);
+        acceptImageView.setOnClickListener(this);
+        switchCameraImageView.setOnClickListener(this);
+        pipRenderer.setOnClickListener(this);
+        outgoingAudioOnlyImageView.setOnClickListener(this);
+        incomingAudioOnlyImageView.setOnClickListener(this);
+        connectedAudioOnlyImageView.setOnClickListener(this);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || OSUtils.isMiui() || OSUtils.isFlyme()) {
+            lytParent.post(() -> {
+                RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) inviteeInfoContainer.getLayoutParams();
+                params.topMargin = (int) (BarUtils.getStatusBarHeight() * 1.2);
+                inviteeInfoContainer.setLayoutParams(params);
+                RelativeLayout.LayoutParams params1 = (RelativeLayout.LayoutParams) minimizeImageView.getLayoutParams();
+                params1.topMargin = BarUtils.getStatusBarHeight();
+                minimizeImageView.setLayoutParams(params1);
+            });
+
+            pipRenderer.post(() -> {
+                FrameLayout.LayoutParams params2 = (FrameLayout.LayoutParams) pipRenderer.getLayoutParams();
+                params2.topMargin = (int) (BarUtils.getStatusBarHeight() * 1.2);
+                pipRenderer.setLayoutParams(params2);
+            });
+        }
+//        if(isOutgoing){ //测试崩溃对方是否会停止
+//            lytParent.postDelayed(() -> {
+//                int i = 1 / 0;
+//            }, 10000);
+//        }
+
+    }
+
+
+    @Override
+    public void init() {
+        super.init();
+        CallSession session = gEngineKit.getCurrentSession();
+        if (session != null) {
+            currentState = session.getState();
+        }
+        if (session == null || CallState.Idle == session.getState()) {
+            if (callSingleActivity != null) {
+                callSingleActivity.finish();
+            }
+        } else if (CallState.Connected == session.getState()) {
+            incomingActionContainer.setVisibility(View.GONE);
+            outgoingActionContainer.setVisibility(View.GONE);
+            connectedActionContainer.setVisibility(View.VISIBLE);
+            inviteeInfoContainer.setVisibility(View.GONE);
+            //minimizeImageView.setVisibility(View.VISIBLE);
+            startRefreshTime();
+        } else {
+            if (isOutgoing) {
+                incomingActionContainer.setVisibility(View.GONE);
+                outgoingActionContainer.setVisibility(View.VISIBLE);
+                connectedActionContainer.setVisibility(View.GONE);
+                descTextView.setText(R.string.av_waiting);
+            } else {
+                incomingActionContainer.setVisibility(View.VISIBLE);
+                outgoingActionContainer.setVisibility(View.GONE);
+                connectedActionContainer.setVisibility(View.GONE);
+                descTextView.setText(R.string.av_video_invite);
+                if (currentState == CallState.Incoming) {
+                    View surfaceView = gEngineKit.getCurrentSession().setupLocalVideo(false);
+                    Log.d(TAG, "init surfaceView != null is " + (surfaceView != null) + "; isOutgoing = " + isOutgoing + "; currentState = " + currentState);
+                    if (surfaceView != null) {
+                        localSurfaceView = (SurfaceViewRenderer) surfaceView;
+                        localSurfaceView.setZOrderMediaOverlay(false);
+                        fullscreenRenderer.addView(localSurfaceView);
+                    }
+                }
+            }
+        }
+        if (isFromFloatingView) {
+            didCreateLocalVideoTrack();
+            if (session != null) {
+                didReceiveRemoteVideoTrack(session.mTargetId);
+            }
+        }
+    }
+
+    @Override
+    public void didChangeState(CallState state) {
+        currentState = state;
+        Log.d(TAG, "didChangeState, state = " + state);
+        runOnUiThread(() -> {
+            if (state == CallState.Connected) {
+                handler.removeMessages(WHAT_DELAY_END_CALL);
+                incomingActionContainer.setVisibility(View.GONE);
+                outgoingActionContainer.setVisibility(View.GONE);
+                connectedActionContainer.setVisibility(View.VISIBLE);
+                inviteeInfoContainer.setVisibility(View.GONE);
+                descTextView.setVisibility(View.GONE);
+                //minimizeImageView.setVisibility(View.VISIBLE);
+                // 开启计时器
+                startRefreshTime();
+            } else {
+                // do nothing now
+            }
+        });
+    }
+
+    @Override
+    public void didChangeMode(Boolean isAudio) {
+        runOnUiThread(() -> callSingleActivity.switchAudio());
+    }
+
+
+    @Override
+    public void didCreateLocalVideoTrack() {
+        if (localSurfaceView == null) {
+            View surfaceView = gEngineKit.getCurrentSession().setupLocalVideo(true);
+            if (surfaceView != null) {
+                localSurfaceView = (SurfaceViewRenderer) surfaceView;
+            } else {
+                if (callSingleActivity != null) callSingleActivity.finish();
+                return;
+            }
+        } else {
+            localSurfaceView.setZOrderMediaOverlay(true);
+        }
+        Log.d(TAG,
+                "didCreateLocalVideoTrack localSurfaceView != null is " + (localSurfaceView != null) + "; remoteSurfaceView == null = " + (remoteSurfaceView == null)
+        );
+
+        if (localSurfaceView.getParent() != null) {
+            ((ViewGroup) localSurfaceView.getParent()).removeView(localSurfaceView);
+        }
+        if (isOutgoing && remoteSurfaceView == null) {
+            if (fullscreenRenderer != null && fullscreenRenderer.getChildCount() != 0)
+                fullscreenRenderer.removeAllViews();
+            fullscreenRenderer.addView(localSurfaceView);
+        } else {
+            if (pipRenderer.getChildCount() != 0) pipRenderer.removeAllViews();
+            pipRenderer.addView(localSurfaceView);
+        }
+    }
+
+
+    @Override
+    public void didReceiveRemoteVideoTrack(String userId) {
+        pipRenderer.setVisibility(View.VISIBLE);
+        if (localSurfaceView != null) {
+            localSurfaceView.setZOrderMediaOverlay(true);
+            if (isOutgoing) {
+                if (localSurfaceView.getParent() != null) {
+                    ((ViewGroup) localSurfaceView.getParent()).removeView(localSurfaceView);
+                }
+                pipRenderer.addView(localSurfaceView);
+            }
+        }
+
+
+        View surfaceView = gEngineKit.getCurrentSession().setupRemoteVideo(userId, false);
+        Log.d(TAG, "didReceiveRemoteVideoTrack,surfaceView = " + surfaceView);
+        if (surfaceView != null) {
+            fullscreenRenderer.setVisibility(View.VISIBLE);
+            remoteSurfaceView = (SurfaceViewRenderer) surfaceView;
+            fullscreenRenderer.removeAllViews();
+            if (remoteSurfaceView.getParent() != null) {
+                ((ViewGroup) remoteSurfaceView.getParent()).removeView(remoteSurfaceView);
+            }
+            fullscreenRenderer.addView(remoteSurfaceView);
+        }
+    }
+
+    @Override
+    public void didUserLeave(String userId) {
+
+    }
+
+    @Override
+    public void didError(String error) {
+
+    }
+
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        // 接听
+        CallSession session = gEngineKit.getCurrentSession();
+        if (id == R.id.acceptImageView) {
+            if (session != null && session.getState() == CallState.Incoming) {
+                session.joinHome(session.getRoomId());
+            } else if (session != null) {
+                if (callSingleActivity != null) {
+                    session.sendRefuse();
+                    callSingleActivity.finish();
+                }
+            }
+        }
+        // 挂断电话
+        if (id == R.id.incomingHangupImageView || id == R.id.outgoingHangupImageView || id == R.id.connectedHangupImageView) {
+            if (session != null) {
+                Log.d(TAG, "endCall");
+                SkyEngineKit.Instance().endCall();
+            }
+            if (callSingleActivity != null) callSingleActivity.finish();
+        }
+
+        // 切换摄像头
+        if (id == R.id.switchCameraImageView) {
+            session.switchCamera();
+        }
+        if (id == R.id.pip_video_view) {
+            boolean isFullScreenRemote = fullscreenRenderer.getChildAt(0) == remoteSurfaceView;
+            fullscreenRenderer.removeAllViews();
+            pipRenderer.removeAllViews();
+            if (isFullScreenRemote) {
+                remoteSurfaceView.setZOrderMediaOverlay(true);
+                pipRenderer.addView(remoteSurfaceView);
+                localSurfaceView.setZOrderMediaOverlay(false);
+                fullscreenRenderer.addView(localSurfaceView);
+            } else {
+                localSurfaceView.setZOrderMediaOverlay(true);
+                pipRenderer.addView(localSurfaceView);
+                remoteSurfaceView.setZOrderMediaOverlay(false);
+                fullscreenRenderer.addView(remoteSurfaceView);
+            }
+        }
+
+        // 切换到语音拨打
+        if (id == R.id.outgoingAudioOnlyImageView || id == R.id.incomingAudioOnlyImageView || id == R.id.connectedAudioOnlyImageView) {
+            if (session != null) {
+                if (callSingleActivity != null) callSingleActivity.isAudioOnly = true;
+                session.switchToAudio();
+            }
+        }
+
+        // 小窗
+        /*if (id == R.id.minimizeImageView) {
+            if (callSingleActivity != null) callSingleActivity.showFloatingView();
+        }*/
+    }
+
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        fullscreenRenderer.removeAllViews();
+        pipRenderer.removeAllViews();
+    }
+}

+ 73 - 0
WebRTC/src/main/java/com/wdkl/core/voip/NineGridView.java

@@ -0,0 +1,73 @@
+package com.wdkl.core.voip;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.GridLayout;
+
+import com.wdkl.webrtc.R;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by dds on 2020/7/5.
+ * ddssingsong@163.com
+ */
+public class NineGridView extends GridLayout {
+    private Map<String, View> map = new ConcurrentHashMap<>();
+
+    public NineGridView(Context context) {
+        this(context, null);
+    }
+
+    public NineGridView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    private void init() {
+        setColumnCount(3);
+        setRowCount(3);
+    }
+
+    public void addView(String userId, View view) {
+        map.put(userId, view);
+        resetView();
+    }
+
+
+    public void removeView(String userId) {
+        map.remove(userId);
+        resetView();
+    }
+
+    private void resetView() {
+        this.removeAllViews();
+        int i = 0;
+        Iterator<Map.Entry<String, View>> inter = map.entrySet().iterator();
+        while (inter.hasNext()) {
+            Map.Entry<String, View> entry = inter.next();
+            String key = entry.getKey();
+            View view = map.get(key);
+            GridLayout.Spec rowSpec = GridLayout.spec(i / 3, 1f);
+            GridLayout.Spec columnSpec = GridLayout.spec(i % 3, 1f);
+            GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(rowSpec, columnSpec);
+            layoutParams.height = 0;
+            layoutParams.width = 0;
+            if (i / 3 == 0)
+                layoutParams.bottomMargin = getResources().getDimensionPixelSize(R.dimen.dp_2);
+            if (i % 3 == 1) {
+                layoutParams.leftMargin = getResources().getDimensionPixelSize(R.dimen.dp_2);
+                layoutParams.rightMargin = getResources().getDimensionPixelSize(R.dimen.dp_2);
+            }
+            this.addView(view, layoutParams);
+            i++;
+        }
+
+
+    }
+
+
+}

+ 153 - 0
WebRTC/src/main/java/com/wdkl/core/voip/RomUtil.java

@@ -0,0 +1,153 @@
+package com.wdkl.core.voip;
+
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Created by dds on 2018/3/20.
+ */
+
+public class RomUtil {
+    private static final String TAG = "RomUtil";
+
+    public static final String ROM_MIUI = "MIUI";
+    public static final String ROM_EMUI = "EMUI";
+    public static final String ROM_FLYME = "FLYME";
+    public static final String ROM_OPPO = "OPPO";
+    public static final String ROM_SMARTISAN = "SMARTISAN";
+
+    public static final String ROM_VIVO = "VIVO";
+    public static final String ROM_QIKU = "QIKU";
+
+    public static final String ROM_LENOVO = "LENOVO";
+    public static final String ROM_SAMSUNG = "SAMSUNG";
+
+    private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name";
+    private static final String KEY_VERSION_EMUI = "ro.build.version.emui";
+    private static final String KEY_VERSION_OPPO = "ro.build.version.opporom";
+    private static final String KEY_VERSION_SMARTISAN = "ro.smartisan.version";
+    private static final String KEY_VERSION_VIVO = "ro.vivo.os.version";
+    private static final String KEY_VERSION_GIONEE = "ro.gn.sv.version";
+    private static final String KEY_VERSION_LENOVO = "ro.lenovo.lvp.version";
+    private static final String KEY_VERSION_FLYME = "ro.build.display.id";
+
+
+    private static final String KEY_EMUI_VERSION_CODE = "ro.build.hw_emui_api_level";
+
+    private static final String KEY_MIUI_VERSION_CODE = "ro.miui.ui.version.code";
+    private static final String KEY_MIUI_HANDY_MODE_SF = "ro.miui.has_handy_mode_sf";
+    private static final String KEY_MIUI_REAL_BLUR = "ro.miui.has_real_blur";
+
+    private static final String KEY_FLYME_PUBLISHED = "ro.flyme.published";
+    private static final String KEY_FLYME_FLYME = "ro.meizu.setupwizard.flyme";
+
+    private static final String KEY_FLYME_ICON_FALG = "persist.sys.use.flyme.icon";
+    private static final String KEY_FLYME_SETUP_FALG = "ro.meizu.setupwizard.flyme";
+    private static final String KEY_FLYME_PUBLISH_FALG = "ro.flyme.published";
+
+    private static final String KEY_VIVO_OS_NAME = "ro.vivo.os.name";
+    private static final String KEY_VIVO_OS_VERSION = "ro.vivo.os.version";
+    private static final String KEY_VIVO_ROM_VERSION = "ro.vivo.rom.version";
+
+    public static boolean isEmui() {
+        return check(ROM_EMUI);
+    }
+
+    public static boolean isMiui() {
+        return check(ROM_MIUI);
+    }
+
+    public static boolean isVivo() {
+        return check(ROM_VIVO);
+    }
+
+    public static boolean isOppo() {
+        return check(ROM_OPPO);
+    }
+
+    public static boolean isFlyme() {
+        return check(ROM_FLYME);
+    }
+
+    public static boolean isQiku() {
+        return check(ROM_QIKU) || check("360");
+    }
+
+    public static boolean isSmartisan() {
+        return check(ROM_SMARTISAN);
+    }
+
+    private static String sName;
+
+    public static String getName() {
+        if (sName == null) {
+            check("");
+        }
+        return sName;
+    }
+
+    private static String sVersion;
+
+    public static String getVersion() {
+        if (sVersion == null) {
+            check("");
+        }
+        return sVersion;
+    }
+
+    public static boolean check(String rom) {
+        if (sName != null) {
+            return sName.equals(rom);
+        }
+
+        if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_MIUI))) {
+            sName = ROM_MIUI;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_EMUI))) {
+            sName = ROM_EMUI;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_OPPO))) {
+            sName = ROM_OPPO;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_VIVO))) {
+            sName = ROM_VIVO;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_SMARTISAN))) {
+            sName = ROM_SMARTISAN;
+        } else {
+            sVersion = Build.DISPLAY;
+            if (sVersion.toUpperCase().contains(ROM_FLYME)) {
+                sName = ROM_FLYME;
+            } else {
+                sVersion = Build.UNKNOWN;
+                sName = Build.MANUFACTURER.toUpperCase();
+            }
+        }
+        return sName.equals(rom);
+    }
+
+    public static String getProp(String name) {
+        String line = null;
+        BufferedReader input = null;
+        try {
+            Process p = Runtime.getRuntime().exec("getprop " + name);
+            input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
+            line = input.readLine();
+            input.close();
+        } catch (IOException ex) {
+            Log.e(TAG, "Unable to read prop " + name, ex);
+            return null;
+        } finally {
+            if (input != null) {
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return line;
+    }
+
+}

+ 265 - 0
WebRTC/src/main/java/com/wdkl/core/voip/SettingsCompat.java

@@ -0,0 +1,265 @@
+package com.wdkl.core.voip;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+
+/**
+ * Created by dds on 2018/3/20.
+ */
+
+public class SettingsCompat {
+    private static final String TAG = "ezy-settings-compat";
+    public static final int REQUEST_SYSTEM_ALERT_WINDOW = 11001;
+
+    private static final int OP_WRITE_SETTINGS = 23;
+    private static final int OP_SYSTEM_ALERT_WINDOW = 24;
+
+    public static boolean canDrawOverlays(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return Settings.canDrawOverlays(context);
+        } else {
+            return checkOp(context, OP_SYSTEM_ALERT_WINDOW);
+        }
+    }
+
+    public static boolean canWriteSettings(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return Settings.System.canWrite(context);
+        } else {
+            return checkOp(context, OP_WRITE_SETTINGS);
+        }
+    }
+
+    public static boolean setDrawOverlays(Context context, boolean allowed) {
+        return setMode(context, OP_SYSTEM_ALERT_WINDOW, allowed);
+    }
+
+    public static boolean setWriteSettings(Context context, boolean allowed) {
+        return setMode(context, OP_WRITE_SETTINGS, allowed);
+    }
+
+    public static void manageDrawOverlays(Activity context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            try {
+                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
+                intent.setData(Uri.parse("package:" + context.getPackageName()));
+                context.startActivityForResult(intent, REQUEST_SYSTEM_ALERT_WINDOW);
+                return;
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+        if (manageDrawOverlaysForRom(context)) {
+            Log.d("SettingsCompat", "打开设置页面");
+        }
+
+    }
+
+    public static void manageWriteSettings(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
+            intent.setData(Uri.parse("package:" + context.getPackageName()));
+            context.startActivity(intent);
+        }
+    }
+
+    private static boolean manageDrawOverlaysForRom(Context context) {
+        if (RomUtil.isMiui()) {
+            return manageDrawOverlaysForMiui(context);
+        }
+        if (RomUtil.isEmui()) {
+            return manageDrawOverlaysForEmui(context);
+        }
+        if (RomUtil.isFlyme()) {
+            return manageDrawOverlaysForFlyme(context);
+        }
+        if (RomUtil.isOppo()) {
+            return manageDrawOverlaysForOppo(context);
+        }
+        if (RomUtil.isVivo()) {
+            return manageDrawOverlaysForVivo(context);
+        }
+        if (RomUtil.isQiku()) {
+            return manageDrawOverlaysForQihu(context);
+        }
+        if (RomUtil.isSmartisan()) {
+            return manageDrawOverlaysForSmartisan(context);
+        }
+        return false;
+    }
+
+
+    private static boolean checkOp(Context context, int op) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                Method method = AppOpsManager.class.getDeclaredMethod("checkOp", int.class, int.class, String.class);
+                return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+            }
+        }
+        return false;
+    }
+
+    // 可设置Android 4.3/4.4的授权状态
+    private static boolean setMode(Context context, int op, boolean allowed) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            return false;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                @SuppressLint("PrivateApi") Method method = AppOpsManager.class.getDeclaredMethod("setMode", int.class, int.class, String.class, int.class);
+                method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName(), allowed ? AppOpsManager.MODE_ALLOWED : AppOpsManager
+                        .MODE_IGNORED);
+                return true;
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+
+            }
+        }
+        return false;
+    }
+
+    private static boolean startSafely(Context context, Intent intent) {
+        if (context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0) {
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(intent);
+            return true;
+        } else {
+            Log.e(TAG, "Intent is not available! " + intent);
+            return false;
+        }
+    }
+
+    // 小米
+    private static boolean manageDrawOverlaysForMiui(Context context) {
+        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
+        intent.putExtra("extra_pkgname", context.getPackageName());
+        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            Intent intent1 = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+            intent1.setData(Uri.fromParts("package", context.getPackageName(), null));
+            return startSafely(context, intent1);
+        }
+        return false;
+    }
+
+    private final static String HUAWEI_PACKAGE = "com.huawei.systemmanager";
+
+    // 华为
+    private static boolean manageDrawOverlaysForEmui(Context context) {
+        Intent intent = new Intent();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            intent.setClassName(HUAWEI_PACKAGE, "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity");
+            if (startSafely(context, intent)) {
+                return true;
+            }
+        }
+        // Huawei Honor P6|4.4.4|3.0
+        intent.setClassName(HUAWEI_PACKAGE, "com.huawei.notificationmanager.ui.NotificationManagmentActivity");
+        intent.putExtra("showTabsNumber", 1);
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        intent.setClassName(HUAWEI_PACKAGE, "com.huawei.permissionmanager.ui.MainActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        return false;
+    }
+
+    // VIVO
+    private static boolean manageDrawOverlaysForVivo(Context context) {
+        // 不支持直接到达悬浮窗设置页,只能到 i管家 首页
+        Intent intent = new Intent("com.iqoo.secure");
+        intent.setClassName("com.iqoo.secure", "com.iqoo.secure.MainActivity");
+        // com.iqoo.secure.ui.phoneoptimize.SoftwareManagerActivity
+        // com.iqoo.secure.ui.phoneoptimize.FloatWindowManager
+        return startSafely(context, intent);
+    }
+
+    // OPPO
+    private static boolean manageDrawOverlaysForOppo(Context context) {
+        Intent intent = new Intent();
+        intent.putExtra("packageName", context.getPackageName());
+        // OPPO A53|5.1.1|2.1
+        intent.setAction("com.oppo.safe");
+        intent.setClassName("com.oppo.safe", "com.oppo.safe.permission.floatwindow.FloatWindowListActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        // OPPO R7s|4.4.4|2.1
+        intent.setAction("com.color.safecenter");
+        intent.setClassName("com.color.safecenter", "com.color.safecenter.permission.floatwindow.FloatWindowListActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        intent.setAction("com.coloros.safecenter");
+        intent.setClassName("com.coloros.safecenter", "com.coloros.safecenter.sysfloatwindow.FloatWindowListActivity");
+        return startSafely(context, intent);
+    }
+
+    // 魅族
+    private static boolean manageDrawOverlaysForFlyme(Context context) {
+        Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
+        intent.setClassName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity");
+        intent.putExtra("packageName", context.getPackageName());
+        return startSafely(context, intent);
+    }
+
+    // 360
+    private static boolean manageDrawOverlaysForQihu(Context context) {
+        Intent intent = new Intent();
+        intent.setClassName("com.android.settings", "com.android.settings.Settings$OverlaySettingsActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        intent.setClassName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity");
+        return startSafely(context, intent);
+    }
+
+    // 锤子
+    private static boolean manageDrawOverlaysForSmartisan(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return false;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            // 锤子 坚果|5.1.1|2.5.3
+            Intent intent = new Intent("com.smartisanos.security.action.SWITCHED_PERMISSIONS_NEW");
+            intent.setClassName("com.smartisanos.security", "com.smartisanos.security.SwitchedPermissions");
+            intent.putExtra("index", 17); // 不同版本会不一样
+            return startSafely(context, intent);
+        } else {
+            // 锤子 坚果|4.4.4|2.1.2
+            Intent intent = new Intent("com.smartisanos.security.action.SWITCHED_PERMISSIONS");
+            intent.setClassName("com.smartisanos.security", "com.smartisanos.security.SwitchedPermissions");
+            intent.putExtra("permission", new String[]{Manifest.permission.SYSTEM_ALERT_WINDOW});
+
+            //        Intent intent = new Intent("com.smartisanos.security.action.MAIN");
+            //        intent.setClassName("com.smartisanos.security", "com.smartisanos.security.MainActivity");
+            return startSafely(context, intent);
+        }
+    }
+}

+ 321 - 0
WebRTC/src/main/java/com/wdkl/core/voip/SingleCallFragment.java

@@ -0,0 +1,321 @@
+package com.wdkl.core.voip;
+
+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.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Chronometer;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.wdkl.core.ui.event.MsgEvent;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+/**
+ * <pre>
+ *     author : Jasper
+ *     e-mail : 229605030@qq.com
+ *     time   : 2021/02/01
+ *     desc   :
+ * </pre>
+ */
+public abstract class SingleCallFragment extends Fragment {
+    private static final String TAG = "SingleCallFragment";
+    ImageView minimizeImageView;
+    ImageView portraitImageView;// 用户头像
+    TextView nameTextView; // 用户昵称
+    TextView descTextView;  // 状态提示用语
+    Chronometer durationTextView; // 通话时长
+
+    ImageView outgoingHangupImageView;
+    ImageView incomingHangupImageView;
+    ImageView acceptImageView;
+    TextView tvStatus;
+    View outgoingActionContainer;
+    View incomingActionContainer;
+    View connectedActionContainer;
+
+    View lytParent;
+
+    boolean isOutgoing = false;
+
+    SkyEngineKit gEngineKit;
+
+
+    CallSingleActivity callSingleActivity;
+
+    CallHandler handler;
+    boolean endWithNoAnswerFlag = false;
+    boolean isConnectionClosed = false;
+
+    public static final int WHAT_DELAY_END_CALL = 0x01;
+
+    public static final int WHAT_NO_NET_WORK_END_CALL = 0x02;
+
+    EnumType.CallState currentState;
+    HeadsetPlugReceiver headsetPlugReceiver;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRetainInstance(true);
+        if (!EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().register(this);
+        }
+        handler = new CallHandler();
+    }
+
+    @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 onDestroyView() {
+        if (durationTextView != null)
+            durationTextView.stop();
+        refreshMessage(true);
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onDestroy() {
+        handler.removeCallbacksAndMessages(null);
+        if (EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().unregister(this);
+        }
+        super.onDestroy();
+    }
+
+
+    abstract int getLayout();
+
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    public void onEvent(MsgEvent<Object> messageEvent) {
+        int code = messageEvent.getCode();
+        Log.d(TAG, "onEvent code = $code; endWithNoAnswerFlag = $endWithNoAnswerFlag");
+        if (code == MsgEvent.CODE_ON_CALL_ENDED) {
+            if (endWithNoAnswerFlag) {
+                didCallEndWithReason(EnumType.CallEndReason.Timeout);
+            } else if (isConnectionClosed) {
+                didCallEndWithReason(EnumType.CallEndReason.SignalError);
+            } else {
+                if (callSingleActivity != null) {
+                    callSingleActivity.finish();
+                }
+            }
+        } else if (code == MsgEvent.CODE_ON_REMOTE_RING) {
+            descTextView.setText("对方已响铃");
+        }
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        callSingleActivity = (CallSingleActivity) getActivity();
+        if (callSingleActivity != null) {
+            isOutgoing = callSingleActivity.isOutgoing();
+            gEngineKit = callSingleActivity.getEngineKit();
+            headsetPlugReceiver = new HeadsetPlugReceiver();
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_HEADSET_PLUG);
+            callSingleActivity.registerReceiver(headsetPlugReceiver, filter);
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        callSingleActivity.unregisterReceiver(headsetPlugReceiver);  //注销监听
+        callSingleActivity = 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);
+        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);
+
+        durationTextView.setVisibility(View.GONE);
+//        nameTextView.setText();
+//        portraitImageView.setImageResource(R.mipmap.icon_default_header);
+        if (isOutgoing) {
+            handler.sendEmptyMessageDelayed(WHAT_DELAY_END_CALL, 30 * 1000);//30s之后未接通,则挂断电话
+        }
+    }
+
+    public void init() {
+    }
+
+    // ======================================界面回调================================
+    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();
+            }
+
+        }, 1500);
+    }
+
+    public void didChangeState(EnumType.CallState state) {
+
+    }
+
+    public void didChangeMode(Boolean isAudio) {
+    }
+
+    public void didCreateLocalVideoTrack() {
+    }
+
+    public void didReceiveRemoteVideoTrack(String userId) {
+    }
+
+    public void didUserLeave(String userId) {
+    }
+
+    public void didError(String error) {
+    }
+
+    public void didDisconnected(String error) {
+        handler.sendEmptyMessage(WHAT_NO_NET_WORK_END_CALL);
+    }
+
+    private void refreshMessage(Boolean isForCallTime) {
+        if (callSingleActivity == null) {
+            return;
+        }
+        // 刷新消息; demo中没有消息,不用处理这儿快逻辑
+    }
+
+    public void startRefreshTime() {
+        CallSession session = SkyEngineKit.Instance().getCurrentSession();
+        if (session == null) return;
+        if (durationTextView != null) {
+            durationTextView.setVisibility(View.VISIBLE);
+            durationTextView.setBase(SystemClock.elapsedRealtime() - (System.currentTimeMillis() - session.getStartTime()));
+            durationTextView.start();
+        }
+    }
+
+    void runOnUiThread(Runnable runnable) {
+        if (callSingleActivity != null) {
+            callSingleActivity.runOnUiThread(runnable);
+        }
+    }
+
+    class CallHandler extends Handler {
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            if (msg.what == WHAT_DELAY_END_CALL) {
+                if (currentState != EnumType.CallState.Connected) {
+                    endWithNoAnswerFlag = true;
+                    if (callSingleActivity != null) {
+                        SkyEngineKit.Instance().endCall();
+                    }
+                }
+            } else if (msg.what == WHAT_NO_NET_WORK_END_CALL) {
+                isConnectionClosed = true;
+                if (callSingleActivity != null) {
+                    SkyEngineKit.Instance().endCall();
+                }
+            }
+        }
+
+    }
+
+
+    class HeadsetPlugReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.hasExtra("state")) {
+                CallSession session = SkyEngineKit.Instance().getCurrentSession();
+                if (session == null) {
+                    return;
+                }
+                if (intent.getIntExtra("state", 0) == 0) { //拔出耳机
+                    session.toggleHeadset(false);
+                } else if (intent.getIntExtra("state", 0) == 1) { //插入耳机
+                    session.toggleHeadset(true);
+                }
+            }
+        }
+    }
+}

+ 7 - 0
WebRTC/src/main/java/com/wdkl/core/voip/Utils.java

@@ -0,0 +1,7 @@
+package com.wdkl.core.voip;
+
+public class Utils {
+
+    public static String ACTION_VOIP_RECEIVER = "com.wdkl.voip.Receiver";
+
+}

+ 111 - 0
WebRTC/src/main/java/com/wdkl/core/voip/VoipEvent.java

@@ -0,0 +1,111 @@
+package com.wdkl.core.voip;
+
+import android.media.AudioManager;
+import android.net.Uri;
+import android.util.Log;
+
+import com.wdkl.core.socket.SocketManager;
+import com.wdkl.skywebrtc.inter.ISkyEvent;
+import com.wdkl.webrtc.R;
+
+import java.util.List;
+
+/**
+ * Created by dds on 2019/8/25.
+ * android_shuai@163.com
+ */
+public class VoipEvent implements ISkyEvent {
+    private static final String TAG = "VoipEvent";
+    private AsyncPlayer ringPlayer;
+
+    public VoipEvent() {
+        ringPlayer = new AsyncPlayer(null);
+    }
+
+    @Override
+    public void createRoom(String room, int roomSize) {
+        SocketManager.getInstance().createRoom(room, roomSize);
+    }
+
+    @Override
+    public void sendInvite(String room, List<String> userIds, boolean audioOnly) {
+        SocketManager.getInstance().sendInvite(room, userIds, audioOnly);
+    }
+
+    @Override
+    public void sendRefuse(String room, String inviteId, int refuseType) {
+        SocketManager.getInstance().sendRefuse(room, inviteId, refuseType);
+    }
+
+    @Override
+    public void sendTransAudio(String toId) {
+        SocketManager.getInstance().sendTransAudio(toId);
+    }
+
+    @Override
+    public void sendDisConnect(String room, String toId, boolean isCrashed) {
+        SocketManager.getInstance().sendDisconnect(room, toId);
+    }
+
+    @Override
+    public void sendCancel(String mRoomId, List<String> toIds) {
+        SocketManager.getInstance().sendCancel(mRoomId, toIds);
+    }
+
+
+    @Override
+    public void sendJoin(String room) {
+        SocketManager.getInstance().sendJoin(room);
+    }
+
+    @Override
+    public void sendRingBack(String targetId, String room) {
+        SocketManager.getInstance().sendRingBack(targetId, room);
+    }
+
+    @Override
+    public void sendLeave(String room, String userId) {
+        SocketManager.getInstance().sendLeave(room, userId);
+    }
+
+
+    @Override
+    public void sendOffer(String userId, String sdp) {
+        SocketManager.getInstance().sendOffer(userId, sdp);
+    }
+
+    @Override
+    public void sendAnswer(String userId, String sdp) {
+        SocketManager.getInstance().sendAnswer(userId, sdp);
+
+    }
+
+    @Override
+    public void sendIceCandidate(String userId, String id, int label, String candidate) {
+        SocketManager.getInstance().sendIceCandidate(userId, id, label, candidate);
+    }
+
+    @Override
+    public void onRemoteRing() {
+
+    }
+
+
+    //==============================================================================
+    @Override
+    public void shouldStartRing(boolean isComing) {
+        if (isComing) {
+            //Uri uri = Uri.parse("android.resource://" + SocketManager.getInstance().getContext().getPackageName() + "/" + R.raw.incoming_call_ring);
+            //ringPlayer.play(SocketManager.getInstance().getContext(), uri, true, AudioManager.STREAM_RING);
+        } else {
+            //Uri uri = Uri.parse("android.resource://" + SocketManager.getInstance().getContext().getPackageName() + "/" + R.raw.wr_ringback);
+            //ringPlayer.play(SocketManager.getInstance().getContext(), R.raw.wr_ringback, true, AudioManager.STREAM_MUSIC);
+        }
+    }
+
+    @Override
+    public void shouldStopRing() {
+        Log.d(TAG, "shouldStopRing begin");
+        //ringPlayer.stop();
+    }
+}

+ 232 - 0
WebRTC/src/main/java/com/wdkl/core/voip/VoipReceiver.java

@@ -0,0 +1,232 @@
+package com.wdkl.core.voip;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.blankj.utilcode.util.LogUtils;
+import com.wdkl.core.base.BaseActivity;
+import com.wdkl.core.socket.SocketManager;
+import com.wdkl.core.util.ActivityStackManager;
+import com.wdkl.permission.Permissions;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+import com.tapadoo.alerter.Alerter;
+
+import java.util.ArrayList;
+
+/**
+ * Created by dds on 2019/8/25.
+ * android_shuai@163.com
+ */
+public class VoipReceiver extends BroadcastReceiver {
+    private static final String TAG = "VoipReceiver";
+    private AsyncPlayer ringPlayer;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if (Utils.ACTION_VOIP_RECEIVER.equals(action)) {
+            String room = intent.getStringExtra("room");
+            boolean audioOnly = intent.getBooleanExtra("audioOnly", true);
+            String inviteId = intent.getStringExtra("inviteId");
+            String inviteUserName = intent.getStringExtra("inviteUserName");
+            String userList = intent.getStringExtra("userList");
+            String[] list = userList.split(",");
+            SkyEngineKit.init(new VoipEvent());
+            //todo 处理邀请人名称
+            if (inviteUserName == null) {
+                inviteUserName = "p2pChat";
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                //if (com.wdkl.core.util.Utils.isAppRunningForeground(context)) {
+                    onForegroundOrBeforeVersionO(context, room, userList, inviteId, audioOnly, inviteUserName, true);
+                //} else {
+                //    onBackgroundAfterVersionO(context, room, userList, inviteId, audioOnly, inviteUserName);
+                //}
+            } else {
+                onForegroundOrBeforeVersionO(context,
+                        room,
+                        userList,
+                        inviteId,
+                        audioOnly,
+                        inviteUserName,
+                        com.wdkl.core.util.Utils.isAppRunningForeground(context)
+                );
+            }
+        }
+    }
+
+    private void onBackgroundAfterVersionO(Context context,
+            String room, String userList,
+            String inviteId, Boolean audioOnly, String inviteUserName
+    ) {
+        String[] strArr = userList.split(",");
+        ArrayList<String> list = new ArrayList<>();
+        for (String str : strArr)
+            list.add(str);
+        SkyEngineKit.init(new VoipEvent());
+        //BaseActivity activity = (BaseActivity) ActivityStackManager.getInstance().getTopActivity();
+        // 权限检测
+        String[] per;
+        if (audioOnly) {
+            per = new String[]{Manifest.permission.RECORD_AUDIO};
+        } else {
+            per = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+        }
+        boolean hasPermission = true;  //Permissions.has(activity, per);
+        if (hasPermission) {
+            onBackgroundHasPermission(context, room, list, inviteId, audioOnly, inviteUserName);
+        } /*else {
+            CallForegroundNotification notification = new CallForegroundNotification(SocketManager.getInstance().getContext());
+            notification.sendRequestIncomingPermissionsNotification(
+                    activity,
+                    room,
+                    userList,
+                    inviteId,
+                    inviteUserName,
+                    audioOnly
+            );
+        }*/
+    }
+
+    private void onBackgroundHasPermission(
+            Context context, String room, ArrayList<String> list,
+            String inviteId, Boolean audioOnly, String inviteUserName) {
+        boolean b = SkyEngineKit.Instance().startInCall(SocketManager.getInstance().getContext(), room, inviteId, audioOnly);
+        LogUtils.dTag(TAG, "onBackgroundHasPermission b = " + b );
+        if (b) {
+            //App.getInstance().setOtherUserId(inviteId);
+            if (list.size() == 1) {
+                CallForegroundNotification notification = new CallForegroundNotification(SocketManager.getInstance().getContext());
+                notification.sendIncomingCallNotification(
+                        SocketManager.getInstance().getContext(),
+                        inviteId,
+                        false,
+                        inviteUserName,
+                        audioOnly,
+                        true
+                );
+            }
+        }
+    }
+
+    private void onForegroundOrBeforeVersionO(Context context,
+            String room, String userList,
+            String inviteId, Boolean audioOnly, String inviteUserName, Boolean isForeGround
+    ) {
+        String[] strArr = userList.split(",");
+        ArrayList<String> list = new ArrayList<>();
+        for (String str : strArr)
+            list.add(str);
+        SkyEngineKit.init(new VoipEvent());
+        //BaseActivity activity = (BaseActivity) ActivityStackManager.getInstance().getTopActivity();
+        // 权限检测
+        String[] per;
+        if (audioOnly) {
+            per = new String[]{Manifest.permission.RECORD_AUDIO};
+        } else {
+            per = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+        }
+        boolean hasPermission = true;  //Permissions.has(activity, per);
+        LogUtils.dTag(TAG, "onForegroundOrBeforeVersionO hasPermission = " + hasPermission + ", isForeGround = " + isForeGround);
+        if (hasPermission) {
+            onHasPermission(context, room, list, inviteId, audioOnly, inviteUserName);
+        } /*else {
+
+            ringPlayer = new AsyncPlayer(null);
+            shouldStartRing(true); //来电先响铃
+            if (isForeGround) {
+                Alerter.create(activity).setTitle("来电通知")
+                        .setText(
+                                "您收到" + inviteUserName + "的来电邀请,请允许"
+                                        + (audioOnly ? "录音"
+                                        : "录音和相机") + "权限来通话"
+                        )
+                        .enableSwipeToDismiss()
+                        .setBackgroundColorRes(R.color.colorAccent) // or setBackgroundColorInt(Color.CYAN)
+                        .setDuration(60 * 1000)
+                        .addButton("确定", R.style.AlertButtonBgWhite, v -> {
+                            Permissions.request(activity, per, integer -> {
+                                shouldStopRing();
+                                Log.d(TAG, "Permissions.request integer = " + integer);
+                                if (integer == 0) { //权限同意
+                                    onHasPermission(activity, room, list, inviteId, audioOnly, inviteUserName);
+                                } else {
+                                    onPermissionDenied(room, inviteId);
+                                }
+                                Alerter.hide();
+                            });
+                        })
+                        .addButton("取消", R.style.AlertButtonBgWhite, v -> {
+                            shouldStopRing();
+                            onPermissionDenied(room, inviteId);
+                            Alerter.hide();
+                        }).show();
+            } else {
+                CallForegroundNotification notification = new CallForegroundNotification(SocketManager.getInstance().getContext());
+                notification.sendRequestIncomingPermissionsNotification(
+                        activity,
+                        room,
+                        userList,
+                        inviteId,
+                        inviteUserName,
+                        audioOnly
+                );
+            }
+
+        }*/
+    }
+
+    private void onHasPermission(
+            Context context, String room, ArrayList<String> list,
+            String inviteId, Boolean audioOnly, String inviteUserName
+    ) {
+        boolean b = SkyEngineKit.Instance().startInCall(SocketManager.getInstance().getContext(), room, inviteId, audioOnly);
+        LogUtils.dTag(TAG, "onHasPermission b = " + b);
+        if (b) {
+            //App.getInstance().setOtherUserId(inviteId);
+            LogUtils.dTag(TAG, "onHasPermission list.size() = " + list.size());
+            if (list.size() == 1) {
+                //以视频电话拨打,切换到音频或重走这里,结束掉上一个,防止对方挂断后,下边还有一个通话界面
+//                if (context instanceof CallSingleActivity) {
+//                    ((CallSingleActivity) context).finish();
+//                }
+//                CallSingleActivity.openActivity(context, inviteId, false, inviteUserName, audioOnly, true);
+            } else {
+                // 群聊
+            }
+        } /*else {
+            Activity activity = ActivityStackManager.getInstance().getTopActivity();
+            activity.finish(); //销毁掉刚才拉起的
+        }*/
+    }
+
+    // 权限拒绝
+    private void onPermissionDenied(String room, String inviteId) {
+        SkyEngineKit.Instance().sendRefuseOnPermissionDenied(room, inviteId);//通知对方结束
+        Toast.makeText(SocketManager.getInstance().getContext(), "权限被拒绝,无法通话", Toast.LENGTH_SHORT).show();
+    }
+
+    private void shouldStartRing(boolean isComing) {
+        if (isComing) {
+            //Uri uri = Uri.parse("android.resource://" + SocketManager.getInstance().getContext().getPackageName() + "/" + R.raw.incoming_call_ring);
+            //ringPlayer.play(SocketManager.getInstance().getContext(), uri, true, AudioManager.STREAM_RING);
+        } else {
+            //Uri uri = Uri.parse("android.resource://" + SocketManager.getInstance().getContext().getPackageName() + "/" + R.raw.wr_ringback);
+            //ringPlayer.play(SocketManager.getInstance().getContext(), R.raw.wr_ringback, true, AudioManager.STREAM_MUSIC);
+        }
+    }
+
+    private void shouldStopRing() {
+        Log.d(TAG, "shouldStopRing begin");
+        //ringPlayer.stop();
+    }
+}

+ 37 - 0
WebRTC/src/main/java/com/wdkl/net/HttpRequest.java

@@ -0,0 +1,37 @@
+package com.wdkl.net;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by dds on 2018/4/23.
+ */
+
+public interface HttpRequest {
+
+    /**
+     * get请求
+     *
+     * @param url      url
+     * @param params   params
+     * @param callback callback
+     */
+    void get(String url, Map<String, Object> params, ICallback callback);
+
+    /**
+     * post请求
+     *
+     * @param url      url
+     * @param params   params
+     * @param callback callback
+     */
+    void post(String url, Map<String, Object> params, ICallback callback);
+
+    /**
+     * 设置双向证书
+     *
+     * @param certificate certificate
+     * @param pwd         pwd
+     */
+    void setCertificate(InputStream certificate, String pwd);
+}

+ 47 - 0
WebRTC/src/main/java/com/wdkl/net/HttpRequestPresenter.java

@@ -0,0 +1,47 @@
+package com.wdkl.net;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by dds on 2019/7/3.
+ * android_shuai@163.com
+ */
+public class HttpRequestPresenter implements HttpRequest {
+    protected HttpRequest httpRequest;
+    private static volatile HttpRequestPresenter instance;
+
+    public HttpRequestPresenter(HttpRequest httpRequest) {
+        this.httpRequest = httpRequest;
+    }
+
+    public static void init(HttpRequest httpRequest) {
+        if (null == instance) {
+            synchronized (HttpRequestPresenter.class) {
+                if (null == instance) {
+                    instance = new HttpRequestPresenter(httpRequest);
+                }
+            }
+        }
+    }
+
+    public static HttpRequestPresenter getInstance() {
+        return instance;
+    }
+
+    @Override
+    public void get(String url, Map<String, Object> params, ICallback callback) {
+        httpRequest.get(url, params, callback);
+    }
+
+    @Override
+    public void post(String url, Map<String, Object> params, ICallback callback) {
+        httpRequest.post(url, params, callback);
+    }
+
+    // 设置双向证书
+    @Override
+    public void setCertificate(InputStream certificate, String pwd) {
+        httpRequest.setCertificate(certificate, pwd);
+    }
+}

+ 12 - 0
WebRTC/src/main/java/com/wdkl/net/ICallback.java

@@ -0,0 +1,12 @@
+package com.wdkl.net;
+
+/**
+ * Created by dds on 2018/4/23.
+ */
+
+public interface ICallback {
+
+    void onSuccess(String result);
+
+    void onFailure(int code, Throwable t);
+}

+ 54 - 0
WebRTC/src/main/java/com/wdkl/net/urlconn/UrlConnRequest.java

@@ -0,0 +1,54 @@
+package com.wdkl.net.urlconn;
+
+
+import com.wdkl.net.HttpRequest;
+import com.wdkl.net.ICallback;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by dds on 2019/12/20.
+ */
+public class UrlConnRequest implements HttpRequest {
+
+
+    public UrlConnRequest() {
+    }
+
+    @Override
+    public void get(String url, Map<String, Object> params, ICallback callback) {
+        try {
+            String param = null;
+            if (params != null) {
+                param = UrlConnUtils.builderUrlParams(params);
+
+            }
+            String s = UrlConnUtils.sendGet(url, param);
+            callback.onSuccess(s);
+
+        } catch (Exception e) {
+            callback.onFailure(-1, e);
+        }
+
+    }
+
+    @Override
+    public void post(String url, Map<String, Object> params, ICallback callback) {
+        try {
+            String postStr = null;
+            if (params != null) {
+                postStr = UrlConnUtils.builderUrlParams(params);
+            }
+            String result = UrlConnUtils.sendPost(url, postStr);
+            callback.onSuccess(result);
+        } catch (Exception e) {
+            callback.onFailure(-1, e);
+        }
+    }
+
+    @Override
+    public void setCertificate(InputStream certificate, String pwd) {
+
+    }
+}

+ 263 - 0
WebRTC/src/main/java/com/wdkl/net/urlconn/UrlConnUtils.java

@@ -0,0 +1,263 @@
+package com.wdkl.net.urlconn;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Created by dds on 2019/11/28.
+ * android_shuai@163.com
+ */
+public class UrlConnUtils {
+    private static final String TAG = "dds_UrlConnUtils";
+
+    public static String sendPost(String serverUrl, String formBody) throws Exception {
+        String result;
+        DataOutputStream out;
+        URL url = new URL(serverUrl);
+        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+        if (serverUrl.startsWith("https")) {
+            trustAllHosts(connection);
+            connection.setHostnameVerifier(DO_NOT_VERIFY);
+        }
+        connection.setDoInput(true);
+        connection.setDoOutput(true);
+        connection.setRequestMethod("POST");
+        connection.setUseCaches(false);
+        connection.setInstanceFollowRedirects(true);
+        connection.addRequestProperty("Content-Type", "application/json");
+        connection.connect();
+        out = new DataOutputStream(connection.getOutputStream());
+        if (formBody != null && !"".equals(formBody)) {
+            out.writeBytes(formBody);
+        }
+        out.flush();
+        int responseCode = connection.getResponseCode();
+        if (responseCode >= 200 && responseCode < 300) {
+            InputStream inputStream = connection.getInputStream();
+            result = inputStream2String(inputStream);
+        } else {
+            throw new Exception(String.format("response code:%d, error msg:%s", responseCode, connection.getResponseMessage()));
+        }
+        connection.disconnect();
+        out.close();
+        return result;
+    }
+
+    public static String sendGet(String serverUrl, String param) throws Exception {
+        String result;
+        String reqUrl = serverUrl + (param == null ? "" : ("?" + param));
+        URL url = new URL(reqUrl);
+
+        // ---------------------------------https--------------------------
+//        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+//        if (serverUrl.startsWith("https")) {
+//            trustAllHosts(connection);
+//            connection.setHostnameVerifier(DO_NOT_VERIFY);
+//        }
+        // http
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        connection.setDoInput(true);
+        connection.setDoOutput(true);
+        connection.setRequestMethod("GET");
+        connection.setUseCaches(false);
+        connection.setInstanceFollowRedirects(true);
+        connection.addRequestProperty("Content-Type", "application/json");
+        connection.connect();
+        int responseCode = connection.getResponseCode();
+        if (responseCode >= 200 && responseCode < 300) {
+            InputStream inputStream = connection.getInputStream();
+            result = inputStream2String(inputStream);
+        } else {
+            throw new Exception(String.format("response code:%d, error msg:%s", responseCode, connection.getResponseMessage()));
+        }
+        connection.disconnect();
+        return result;
+    }
+
+    public static boolean download(String u, String path) {
+        try {
+            URL url = new URL(u);
+            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+            trustAllHosts(connection);
+            connection.setHostnameVerifier(DO_NOT_VERIFY);
+            connection.setDoInput(true);
+            connection.setDoOutput(true);
+            connection.setRequestMethod("GET");
+            connection.setUseCaches(false);
+            connection.setInstanceFollowRedirects(true);
+            //可设置请求头
+            connection.setRequestProperty("Content-Type", "application/octet-stream");
+            connection.setRequestProperty("Connection", "Keep-Alive");// 维持长连接
+            connection.setRequestProperty("Charset", "UTF-8");
+            connection.connect();
+            byte[] file = input2byte(connection.getInputStream());
+            File file1 = writeBytesToFile(file, path);
+            if (file1.exists()) {
+                return true;
+            }
+        } catch (Exception e) {
+            return false;
+        }
+        return false;
+    }
+
+    /**
+     * 构建json参数
+     */
+    public static String builderJsonParams(Map<String, Object> params) {
+        JSONObject jsonObject;
+        try {
+            Set<String> keySet = params.keySet();
+            List<String> keyList = new ArrayList<>(keySet);
+            Collections.sort(keyList);
+            jsonObject = new JSONObject();
+            for (String key : keyList) {
+                Object value = params.get(key);
+                if (value == null || "".equals(value)) {
+                    continue;
+                }
+                jsonObject.put(key, String.valueOf(params.get(key)));
+            }
+        } catch (JSONException e) {
+            return null;
+        }
+        return jsonObject.toString();
+    }
+
+    /**
+     * 构建post参数
+     */
+    public static String builderUrlParams(Map<String, Object> params) {
+        StringBuilder sb = new StringBuilder();
+        Set<String> keySet = params.keySet();
+        List<String> keyList = new ArrayList<>(keySet);
+        Collections.sort(keyList);
+        for (String key : keyList) {
+            Object value = params.get(key);
+            if (value == null || "".equals(value)) {
+                continue;
+            }
+            sb.append(key).append("=").append(params.get(key)).append("&");
+        }
+        if (sb.length() > 0) {
+            return sb.substring(0, sb.length() - 1);
+        }
+        return null;
+    }
+
+    private static byte[] input2byte(InputStream inStream) throws IOException {
+        ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
+        byte[] buff = new byte[100];
+        int rc = 0;
+        while ((rc = inStream.read(buff, 0, 100)) > 0) {
+            swapStream.write(buff, 0, rc);
+        }
+        return swapStream.toByteArray();
+    }
+
+    private static File writeBytesToFile(byte[] b, String outputFile) {
+        File file = null;
+        FileOutputStream os = null;
+        try {
+            file = new File(outputFile);
+            File parentFile = file.getParentFile();
+            if (!parentFile.exists()) {
+                parentFile.mkdirs();
+            }
+            os = new FileOutputStream(file);
+            os.write(b);
+        } catch (Exception var13) {
+            var13.printStackTrace();
+            if (file != null && file.exists()) {
+                file.delete();
+            }
+        } finally {
+            try {
+                if (os != null) {
+                    os.close();
+                }
+            } catch (IOException var12) {
+                var12.printStackTrace();
+            }
+        }
+        return file;
+    }
+
+    private static String inputStream2String(InputStream inputStream) {
+        ByteArrayOutputStream bos = null;
+        byte[] bytes = new byte[1024];
+        int len = 0;
+        try {
+            bos = new ByteArrayOutputStream();
+            while ((len = inputStream.read(bytes)) != -1) {
+                bos.write(bytes, 0, len);
+            }
+            return new String(bos.toByteArray());
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (bos != null) {
+                try {
+                    bos.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return null;
+    }
+
+    private static void trustAllHosts(HttpsURLConnection connection) {
+        try {
+            SSLContext sc = SSLContext.getInstance("TLS");
+            sc.init(null, trustAllCerts, new java.security.SecureRandom());
+            SSLSocketFactory newFactory = sc.getSocketFactory();
+            connection.setSSLSocketFactory(newFactory);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[]{};
+        }
+
+        public void checkClientTrusted(X509Certificate[] chain, String authType) {
+        }
+
+        public void checkServerTrusted(X509Certificate[] chain, String authType) {
+        }
+    }};
+
+    private static final HostnameVerifier DO_NOT_VERIFY = (hostname, session) -> true;
+}

+ 18 - 0
WebRTC/src/main/java/com/wdkl/permission/Consumer.java

@@ -0,0 +1,18 @@
+package com.wdkl.permission;
+
+/**
+ * Represents an operation that accepts a single input argument and returns no
+ * result. Unlike most other functional interfaces, {@code Consumer} is expected
+ * to operate via side-effects.
+ *
+ * @param <T> the type of the input to the operation
+ */
+public interface Consumer<T> {
+
+	/**
+	 * Performs this operation on the given argument.
+	 *
+	 * @param t the input argument
+	 */
+	void accept(T t);
+}

+ 144 - 0
WebRTC/src/main/java/com/wdkl/permission/Permissions.java

@@ -0,0 +1,144 @@
+package com.wdkl.permission;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.Build.VERSION_CODES.M;
+
+/**
+ * Permission-related helpers
+ */
+public class Permissions {
+
+
+    /**
+     * @param callback will be called if request is not canceled, with either
+     *                 {@link PackageManager#PERMISSION_GRANTED} or {@link PackageManager#PERMISSION_DENIED}
+     */
+    public static void request(Activity activity, String permission, Consumer<Integer> callback) {
+        if (Build.VERSION.SDK_INT >= M) {
+            request2(activity, permission, callback);
+        } else {
+            if (has(activity, permission)) {
+                callback.accept(0);
+            } else {
+                callback.accept(-1);
+            }
+        }
+
+    }
+
+    /**
+     * @param callback will be called if request is not canceled, with either
+     *                 {@link PackageManager#PERMISSION_GRANTED} or {@link PackageManager#PERMISSION_DENIED}
+     */
+    public static void request(Activity activity, String[] permissions, Consumer<Integer> callback) {
+        if (Build.VERSION.SDK_INT >= M) {
+            request2(activity, permissions, callback);
+        } else {
+            if (has(activity, permissions)) {
+                callback.accept(0);
+            } else {
+                callback.accept(-1);
+            }
+
+        }
+
+    }
+
+    @RequiresApi(M)
+    public static void request2(Activity activity, String permission, Consumer<Integer> callback) {
+        final FragmentManager fm = activity.getFragmentManager();
+        if (!has(activity, permission)) {
+            fm.beginTransaction().add(new PermissionRequestFragment(new String[]{permission}, callback), null).commitAllowingStateLoss();
+        } else {
+            callback.accept(PERMISSION_GRANTED);
+        }
+    }
+
+    @RequiresApi(M)
+    public static void request2(Activity activity, String[] permissions, Consumer<Integer> callback) {
+        final FragmentManager fm = activity.getFragmentManager();
+        if (!has(activity, permissions)) {
+            fm.beginTransaction().add(new PermissionRequestFragment(permissions, callback), null).commitAllowingStateLoss();
+        } else {
+            callback.accept(PERMISSION_GRANTED);
+        }
+    }
+
+    public static boolean has(Context activity, String... permissions) {
+        List<String> mPermissionListDenied = new ArrayList<>();
+        for (String permission : permissions) {
+            int result = checkPermission(activity, permission);
+            if (result != PERMISSION_GRANTED) {
+                mPermissionListDenied.add(permission);
+            }
+        }
+        return mPermissionListDenied.size() == 0;
+    }
+
+    private static boolean has(Context context, String permission) {
+        return context.checkPermission(permission, Process.myPid(), Process.myUid()) == PERMISSION_GRANTED;
+    }
+
+    private static int checkPermission(Context activity, String permission) {
+        return ContextCompat.checkSelfPermission(activity, permission);
+    }
+
+    @RequiresApi(M)
+    public static class PermissionRequestFragment extends Fragment {
+
+        @SuppressLint("ValidFragment")
+        public PermissionRequestFragment(@NonNull final String[] permissions, @NonNull final Consumer<Integer> callback) {
+            mPermissions = permissions;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onCreate(@Nullable final Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (mPermissions != null) requestPermissions(mPermissions, 0);
+        }
+
+        @Override
+        public void onRequestPermissionsResult(final int request, @NonNull final String[] permissions, @NonNull final int[] results) {
+            getFragmentManager().beginTransaction().remove(this).commit();
+            if (mCallback == null || results.length == 0/* canceled */) return;
+            boolean isGrant = true;
+            for (int result : results) {
+                if (result != PackageManager.PERMISSION_GRANTED) {
+                    isGrant = false;
+                    break;
+                }
+            }
+            mCallback.accept(isGrant ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
+        }
+
+        public PermissionRequestFragment() {
+            mPermissions = null;
+            mCallback = null;
+        }
+
+        private final @Nullable
+        String[] mPermissions;
+        private final @Nullable
+        Consumer<Integer> mCallback;
+    }
+}

BIN
WebRTC/src/main/res/drawable-xhdpi/av_audio_answer.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_audio_answer_hover.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_camera.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_camera_hover.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_default_header.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_float_audio.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_handfree.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_handfree_hover.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_hang_up.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_hang_up_hover.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_minimize.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_mute.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_mute_hover.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_phone.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_trans_audio.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_video_answer.png


BIN
WebRTC/src/main/res/drawable-xhdpi/av_video_answer_hover.png


+ 20 - 0
WebRTC/src/main/res/drawable-xhdpi/bg_btn_white.xml

@@ -0,0 +1,20 @@
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetLeft="4dp"
+    android:insetTop="6dp"
+    android:insetRight="4dp"
+    android:insetBottom="6dp">
+    <shape
+        android:shape="rectangle"
+        android:tint="@color/colorAccent">
+        <stroke
+            android:width="1dp"
+            android:color="#FFFFFF" />
+        <corners android:radius="25dp" />
+        <solid android:color="#FFFFFF" />
+        <padding
+            android:bottom="4dp"
+            android:left="8dp"
+            android:right="8dp"
+            android:top="4dp" />
+    </shape>
+</inset>

+ 10 - 0
WebRTC/src/main/res/drawable/av_audio_answer_selector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/av_audio_answer" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
+    <item android:drawable="@drawable/av_audio_answer_hover" android:state_pressed="false" android:state_selected="true" />
+    <!-- Focused states -->
+    <item android:drawable="@drawable/av_audio_answer_hover" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
+    <!-- Pressed -->
+    <item android:drawable="@drawable/av_audio_answer_hover" android:state_pressed="true" />
+</selector>

+ 16 - 0
WebRTC/src/main/res/drawable/av_float_bg.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+
+    <solid android:color="#ffffff" />
+
+    <stroke
+        android:width="0.5px"
+        android:color="#ffffff" />
+
+    <corners android:radius="3dp" />
+
+    <size
+        android:width="60dp"
+        android:height="80dp" />
+</shape>

+ 10 - 0
WebRTC/src/main/res/drawable/av_hangup_selector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/av_hang_up" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
+    <item android:drawable="@drawable/av_hang_up_hover" android:state_pressed="false" android:state_selected="true" />
+    <!-- Focused states -->
+    <item android:drawable="@drawable/av_hang_up_hover" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
+    <!-- Pressed -->
+    <item android:drawable="@drawable/av_hang_up_hover" android:state_pressed="true" />
+</selector>

+ 5 - 0
WebRTC/src/main/res/drawable/av_mute_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/av_mute_hover" android:state_selected="true" />
+    <item android:drawable="@drawable/av_mute" />
+</selector>

+ 5 - 0
WebRTC/src/main/res/drawable/av_speaker_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/av_handfree_hover" android:state_selected="true" />
+    <item android:drawable="@drawable/av_handfree" />
+</selector>

+ 10 - 0
WebRTC/src/main/res/drawable/av_switch_camera_selector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/av_camera" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
+    <item android:drawable="@drawable/av_camera_hover" android:state_pressed="false" android:state_selected="true" />
+    <!-- Focused states -->
+    <item android:drawable="@drawable/av_camera_hover" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
+    <!-- Pressed -->
+    <item android:drawable="@drawable/av_camera_hover" android:state_pressed="true" />
+</selector>

+ 10 - 0
WebRTC/src/main/res/drawable/av_video_answer_selector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/av_video_answer" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
+    <item android:drawable="@drawable/av_video_answer_hover" android:state_pressed="false" android:state_selected="true" />
+    <!-- Focused states -->
+    <item android:drawable="@drawable/av_video_answer_hover" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
+    <!-- Pressed -->
+    <item android:drawable="@drawable/av_video_answer_hover" android:state_pressed="true" />
+</selector>

+ 9 - 0
WebRTC/src/main/res/drawable/ic_dashboard_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" />
+</vector>

+ 9 - 0
WebRTC/src/main/res/drawable/ic_home_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
+</vector>

+ 9 - 0
WebRTC/src/main/res/drawable/ic_notifications_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
+</vector>

+ 52 - 0
WebRTC/src/main/res/layout/activity_launcher.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+
+    tools:context="com.wdkl.LauncherActivity">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="@style/AppTheme.AppBarOverlay">
+
+        <androidx.appcompat.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="48dp"
+            app:popupTheme="@style/AppTheme.PopupOverlay" />
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:padding="16dp">
+
+        <EditText
+            android:id="@+id/et_user"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:digits="0123456789qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM"
+            android:hint="input your user name " />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="The server side is written by Spring boot" />
+
+        <Button
+            android:id="@+id/button8"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:onClick="java"
+            android:text="ENTER"
+            android:textAllCaps="false" />
+    </LinearLayout>
+
+</LinearLayout>

+ 29 - 0
WebRTC/src/main/res/layout/activity_main.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.google.android.material.bottomnavigation.BottomNavigationView
+        android:id="@+id/nav_view"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="0dp"
+        android:layout_marginEnd="0dp"
+        android:background="?android:attr/windowBackground"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:menu="@menu/bottom_nav_menu" />
+
+    <fragment
+        android:id="@+id/nav_host_fragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toTopOf="@id/nav_view"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 21 - 0
WebRTC/src/main/res/layout/activity_multi_call.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context="com.wdkl.core.voip.CallMultiActivity">
+
+    <FrameLayout
+        android:id="@+id/meeting_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="4"
+        android:background="@color/av_bg_call_black" />
+
+    <include
+        layout="@layout/av_p2p_meeting_action"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+</LinearLayout>

+ 9 - 0
WebRTC/src/main/res/layout/activity_single_call.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.wdkl.core.voip.CallSingleActivity">
+
+</LinearLayout>

+ 64 - 0
WebRTC/src/main/res/layout/av_p2p_audio_incoming.xml

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:paddingLeft="30dp"
+    android:paddingRight="30dp"
+    android:paddingBottom="20dp">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20dp"
+        android:orientation="horizontal">
+
+        <LinearLayout
+            android:id="@+id/hangupLinearLayout"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentLeft="true"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/incomingHangupImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_hangup_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="挂断"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/acceptLinearLayout"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/acceptImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_audio_answer_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="接听"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+    </RelativeLayout>
+
+</LinearLayout>

+ 92 - 0
WebRTC/src/main/res/layout/av_p2p_audio_outgoing.xml

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:orientation="vertical"
+    android:paddingBottom="30dp">
+
+    <Chronometer
+        android:id="@+id/durationTextView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="@android:color/white"
+        tools:visibility="visible" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20dp"
+        android:orientation="horizontal">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:visibility="invisible">
+
+            <ImageView
+                android:id="@+id/muteImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_mute_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="静音"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/outgoingHangupImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_hangup_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="挂断"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:visibility="invisible">
+
+            <ImageView
+                android:id="@+id/speakerImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_speaker_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="免提"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+    </LinearLayout>
+
+</LinearLayout>

+ 113 - 0
WebRTC/src/main/res/layout/av_p2p_meeting_action.xml

@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@android:color/background_dark"
+    android:gravity="center"
+    android:orientation="vertical">
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:visibility="gone">
+
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/muteImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_mute_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="静音"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:layout_marginBottom="20dp"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/switchCameraImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_switch_camera_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="切换摄像头"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:layout_marginBottom="20dp"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/speakerImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_speaker_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="免提"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/hangupLinearLayout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginTop="10dp"
+        android:gravity="center"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/meetingHangupImageView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/av_hangup_selector" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="挂断"
+            android:textColor="@android:color/white" />
+
+    </LinearLayout>
+</LinearLayout>

+ 90 - 0
WebRTC/src/main/res/layout/av_p2p_video_connected_action.xml

@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:orientation="vertical"
+    android:paddingBottom="30dp">
+
+    <Chronometer
+        android:id="@+id/durationTextView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="@android:color/white" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20dp"
+        android:orientation="horizontal">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:visibility="invisible">
+
+            <ImageView
+                android:id="@+id/connectedAudioOnlyImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_phone" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="切到语音通话"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/connectedHangupImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_hangup_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="挂断"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:visibility="invisible">
+
+            <ImageView
+                android:id="@+id/switchCameraImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_switch_camera_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="旋转摄像头"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+    </LinearLayout>
+
+</LinearLayout>

+ 88 - 0
WebRTC/src/main/res/layout/av_p2p_video_incoming_action.xml

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="right"
+    android:orientation="vertical"
+    android:paddingLeft="20dp"
+    android:paddingRight="20dp"
+    android:paddingBottom="30dp">
+
+
+    <LinearLayout
+        android:id="@+id/audioLayout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:gravity="center"
+        android:orientation="vertical"
+        android:visibility="invisible">
+
+        <ImageView
+            android:id="@+id/incomingAudioOnlyImageView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/av_trans_audio" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="切换到语音接听"
+            android:textColor="@android:color/white" />
+
+    </LinearLayout>
+
+
+    <LinearLayout
+        android:id="@+id/hangupLinearLayout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/audioLayout"
+        android:layout_alignParentLeft="true"
+        android:layout_marginTop="20dp"
+        android:gravity="center"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/incomingHangupImageView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/av_hangup_selector" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="挂断"
+            android:textColor="@android:color/white" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/acceptLinearLayout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@id/audioLayout"
+        android:layout_alignLeft="@id/audioLayout"
+        android:layout_alignRight="@id/audioLayout"
+        android:layout_marginTop="20dp"
+        android:gravity="center"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/acceptImageView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:src="@drawable/av_video_answer_selector" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:text="接听"
+            android:textColor="@android:color/white" />
+
+    </LinearLayout>
+
+</RelativeLayout>

+ 54 - 0
WebRTC/src/main/res/layout/av_p2p_video_outgoing_action.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:paddingBottom="30dp">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:orientation="vertical"
+        android:visibility="invisible">
+
+        <ImageView
+            android:id="@+id/outgoingAudioOnlyImageView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/av_trans_audio" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="切换到语音"
+            android:textColor="@android:color/white"
+            android:textSize="10sp" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="120dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:gravity="center_horizontal"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/outgoingHangupImageView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:background="@drawable/av_hangup_selector" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="5dp"
+            android:text="取消"
+            android:textColor="@android:color/white"
+            android:textSize="12sp" />
+
+    </LinearLayout>
+
+</LinearLayout>

+ 39 - 0
WebRTC/src/main/res/layout/av_voip_float_view.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:background="@drawable/av_float_bg"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/audioLinearLayout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/av_media_type"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:src="@drawable/av_float_audio" />
+
+        <TextView
+            android:id="@+id/durationTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:gravity="center"
+            android:text="00:00"
+            android:textColor="#0195ff" />
+    </LinearLayout>
+
+    <FrameLayout
+        android:id="@+id/remoteVideoFrameLayout"
+        android:layout_width="100dp"
+        android:layout_height="140dp"
+        android:visibility="gone" />
+
+</LinearLayout>

+ 76 - 0
WebRTC/src/main/res/layout/fragment_audio.xml

@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/background_dark">
+
+    <ImageView
+        android:id="@+id/minimizeImageView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:padding="20dp"
+        android:src="@drawable/av_minimize"
+        android:visibility="gone"/>
+
+    <LinearLayout
+        android:id="@+id/lytParent"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:gravity="center_horizontal"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/portraitImageView"
+            android:layout_width="120dp"
+            android:layout_height="120dp"
+            android:layout_marginTop="80dp"
+            android:src="@drawable/av_default_header" />
+
+        <TextView
+            android:id="@+id/nameTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:text=""
+            android:textColor="@android:color/white" />
+
+        <TextView
+            android:id="@+id/descTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="5dp"
+            android:text="邀请你进行语音通话"
+            android:textColor="@android:color/white" />
+    </LinearLayout>
+
+    <!--拨出按钮显示-->
+    <include
+        android:id="@+id/outgoingActionContainer"
+        layout="@layout/av_p2p_audio_outgoing"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:visibility="visible" />
+
+    <!--接听按钮显示-->
+    <include
+        android:id="@+id/incomingActionContainer"
+        layout="@layout/av_p2p_audio_incoming"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:visibility="gone" />
+
+    <TextView
+        android:id="@+id/tvStatus"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:layout_marginBottom="130dp"
+        android:textColor="#FFFFFF"
+        android:textSize="16sp" />
+
+</RelativeLayout>

+ 28 - 0
WebRTC/src/main/res/layout/fragment_home.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+
+    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+        android:id="@+id/swipe"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+    <TextView
+        android:id="@+id/no_data"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_centerInParent="true"
+        android:gravity="center"
+        android:text="no data"
+        android:textSize="25sp" />
+
+</RelativeLayout>

+ 12 - 0
WebRTC/src/main/res/layout/fragment_meeting.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/meeting_item_container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.wdkl.core.voip.NineGridView
+        android:id="@+id/grid_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</RelativeLayout>

+ 27 - 0
WebRTC/src/main/res/layout/fragment_room.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+        android:id="@+id/swipe"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
+
+
+    <TextView
+        android:id="@+id/no_data"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_centerInParent="true"
+        android:gravity="center"
+        android:text="no data"
+        android:textSize="25sp" />
+</RelativeLayout>

+ 25 - 0
WebRTC/src/main/res/layout/fragment_setting.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="16dp">
+
+    <TextView
+        android:id="@+id/text_notifications"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:textAlignment="center"
+        android:textSize="20sp" />
+
+    <Button
+        android:id="@+id/exit"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="退出登录" />
+
+
+</LinearLayout>

+ 119 - 0
WebRTC/src/main/res/layout/fragment_video.xml

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/black"
+    tools:ignore="MergeRootFrame">
+
+    <FrameLayout
+        android:id="@+id/fullscreen_video_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_gravity="center" />
+
+    <FrameLayout
+        android:id="@+id/pip_video_view"
+        android:layout_width="100dp"
+        android:layout_height="140dp"
+        android:layout_gravity="top|end"
+        android:layout_marginHorizontal="10dp"
+        android:layout_marginTop="10dp" />
+
+    <RelativeLayout
+        android:id="@+id/lytParent"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@android:color/transparent">
+
+        <LinearLayout
+            android:id="@+id/inviteeInfoContainer"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="20dp"
+            android:layout_marginStart="20dp"
+            android:layout_marginTop="20dp"
+            android:layout_marginEnd="20dp"
+            android:gravity="center_vertical"
+            android:orientation="horizontal"
+            android:visibility="visible">
+
+            <ImageView
+                android:id="@+id/portraitImageView"
+                android:layout_width="60dp"
+                android:layout_height="60dp"
+                android:adjustViewBounds="true"
+                android:scaleType="centerCrop"
+                android:src="@drawable/av_default_header" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="10dp"
+                android:gravity="center_vertical"
+                android:orientation="vertical">
+
+                <TextView
+                    android:id="@+id/nameTextView"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text=""
+                    android:textColor="@android:color/white" />
+
+                <TextView
+                    android:id="@+id/descTextView"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="等待对方接通..."
+                    android:textColor="@android:color/white" />
+            </LinearLayout>
+        </LinearLayout>
+
+        <ImageView
+            android:id="@+id/minimizeImageView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="20dp"
+            android:src="@drawable/av_minimize"
+            android:visibility="gone" />
+
+        <!--拨出控制-->
+        <include
+            android:id="@+id/outgoingActionContainer"
+            layout="@layout/av_p2p_video_outgoing_action"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:visibility="visible" />
+
+        <!--来电控制-->
+        <include
+            android:id="@+id/incomingActionContainer"
+            layout="@layout/av_p2p_video_incoming_action"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:visibility="gone" />
+
+        <!--接通控制-->
+        <include
+            android:id="@+id/connectedActionContainer"
+            layout="@layout/av_p2p_video_connected_action"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:visibility="gone" />
+
+        <TextView
+            android:id="@+id/tvStatus"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="130dp"
+            android:textColor="#FFFFFF"
+            android:textSize="16sp" />
+    </RelativeLayout>
+
+</FrameLayout>

+ 40 - 0
WebRTC/src/main/res/layout/item_rooms.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="76dp"
+    android:background="#ddf2d2">
+
+    <ImageView
+        android:id="@+id/item_user_avatar"
+        android:layout_width="44dp"
+        android:layout_height="44dp"
+        android:layout_centerVertical="true"
+        android:layout_marginStart="16dp"
+        android:background="@color/colorPrimary" />
+
+    <TextView
+        android:id="@+id/item_user_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginStart="16dp"
+        android:layout_toEndOf="@id/item_user_avatar"
+        android:text="ddssingsong"
+        android:textColor="@android:color/black"
+        android:textSize="16sp" />
+
+    <Button
+        android:id="@+id/item_join_room"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true"
+        android:text="进入房间" />
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:layout_alignParentBottom="true"
+        android:background="@android:color/darker_gray" />
+
+</RelativeLayout>

+ 48 - 0
WebRTC/src/main/res/layout/item_users.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="70dp"
+    android:background="#dd82d2">
+
+    <ImageView
+        android:id="@+id/item_user_avatar"
+        android:layout_width="44dp"
+        android:layout_height="44dp"
+        android:layout_centerVertical="true"
+        android:layout_marginStart="16dp"
+        android:background="@android:color/holo_orange_dark" />
+
+    <TextView
+        android:id="@+id/item_user_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_marginStart="16dp"
+        android:layout_toEndOf="@id/item_user_avatar"
+        android:text="ddssingsong"
+        android:textColor="@android:color/black"
+        android:textSize="16sp" />
+
+    <Button
+        android:id="@+id/item_call_audio"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true"
+        android:text="语音" />
+
+    <Button
+        android:id="@+id/item_call_video"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerVertical="true"
+        android:layout_toStartOf="@id/item_call_audio"
+        android:text="视频" />
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_alignParentBottom="true"
+        android:background="@android:color/holo_blue_dark" />
+
+</RelativeLayout>

+ 19 - 0
WebRTC/src/main/res/menu/bottom_nav_menu.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/navigation_user"
+        android:icon="@drawable/ic_home_black_24dp"
+        android:title="@string/title_user" />
+
+    <item
+        android:id="@+id/navigation_room"
+        android:icon="@drawable/ic_dashboard_black_24dp"
+        android:title="@string/title_room" />
+
+    <item
+        android:id="@+id/navigation_setting"
+        android:icon="@drawable/ic_notifications_black_24dp"
+        android:title="@string/title_setting" />
+
+</menu>

+ 12 - 0
WebRTC/src/main/res/menu/menu_room.xml

@@ -0,0 +1,12 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_create"
+        android:icon="@android:drawable/ic_menu_add"
+        android:orderInCategory="100"
+        android:title="@string/action_create_room"
+        app:showAsAction="always" />
+
+
+</menu>

BIN
WebRTC/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
WebRTC/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
WebRTC/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
WebRTC/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
WebRTC/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
WebRTC/src/main/res/raw/incoming_call_ring.mp3


BIN
WebRTC/src/main/res/raw/outgoing_call_ring.mp3


BIN
WebRTC/src/main/res/raw/wr_ringback.wav


+ 9 - 0
WebRTC/src/main/res/values/colors.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#1b82d2</color>       <!--toolbar-->
+    <color name="colorPrimaryDark">#1b82d2</color>   <!--status bar-->
+    <color name="colorAccent">#1b82d2</color>
+
+
+    <color name="av_bg_call_black">#281f1d</color>
+</resources>

+ 6 - 0
WebRTC/src/main/res/values/dimens.xml

@@ -0,0 +1,6 @@
+<resources>
+    <!-- Default screen margins, per the Android Design guidelines. -->
+    <dimen name="activity_horizontal_margin">16dp</dimen>
+    <dimen name="activity_vertical_margin">16dp</dimen>
+    <dimen name="dp_2">2dp</dimen>
+</resources>

+ 0 - 0
WebRTC/src/main/res/values/strings.xml


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio