Browse Source

wdklRTC OK

allen 4 years ago
parent
commit
4b41d0bf0d
100 changed files with 8629 additions and 1 deletions
  1. 1 0
      home/build.gradle
  2. 1 1
      settings.gradle
  3. 5 0
      wdklRTC/.gitignore
  4. 68 0
      wdklRTC/build.gradle
  5. BIN
      wdklRTC/dds.jks
  6. 25 0
      wdklRTC/proguard-rules.pro
  7. 84 0
      wdklRTC/src/main/AndroidManifest.xml
  8. 60 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/App.java
  9. 87 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/LauncherActivity.java
  10. 97 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/MainActivity.java
  11. 23 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/base/BaseActivity.java
  12. 26 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/consts/Urls.java
  13. 48 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/socket/IEvent.java
  14. 15 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/socket/IUserState.java
  15. 500 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/socket/MyWebSocket.java
  16. 358 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/socket/SocketManager.java
  17. 35 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/event/MsgEvent.java
  18. 153 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/room/RoomFragment.java
  19. 46 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/room/RoomInfo.java
  20. 50 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/room/RoomViewModel.java
  21. 37 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/setting/SettingFragment.java
  22. 19 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/setting/SettingViewModel.java
  23. 42 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/user/UserBean.java
  24. 148 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/user/UserListFragment.java
  25. 53 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/user/UserListViewModel.java
  26. 131 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/ActivityStackManager.java
  27. 53 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/CrashHandler.java
  28. 127 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/OSUtils.java
  29. 25 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/StringUtil.java
  30. 53 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/Utils.java
  31. 187 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/AsyncPlayer.java
  32. 121 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/CallForegroundNotification.java
  33. 195 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/CallMultiActivity.java
  34. 339 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/CallSingleActivity.java
  35. 439 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/FloatingVoipService.java
  36. 104 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/FragmentAudio.java
  37. 119 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/FragmentMeeting.java
  38. 295 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/FragmentVideo.java
  39. 73 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/NineGridView.java
  40. 153 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/RomUtil.java
  41. 265 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/SettingsCompat.java
  42. 312 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/SingleCallFragment.java
  43. 13 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/Utils.java
  44. 112 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/VoipEvent.java
  45. 232 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/VoipReceiver.java
  46. 37 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/HttpRequest.java
  47. 47 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/HttpRequestPresenter.java
  48. 12 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/ICallback.java
  49. 54 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/urlconn/UrlConnRequest.java
  50. 263 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/urlconn/UrlConnUtils.java
  51. 18 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/permission/Consumer.java
  52. 144 0
      wdklRTC/src/main/java/com/wdkl/webrtc/demo/permission/Permissions.java
  53. 196 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/AVEngine.java
  54. 12 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/BuildConfig.java
  55. 549 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/CallSession.java
  56. 41 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/EnumType.java
  57. 206 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/SkyEngineKit.java
  58. 43 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/engine/EngineCallback.java
  59. 121 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/engine/IEngine.java
  60. 354 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/engine/webrtc/Peer.java
  61. 696 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/engine/webrtc/WebRTCEngine.java
  62. 11 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/except/NotInitializedException.java
  63. 12 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/inter/ILogEvent.java
  64. 47 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/inter/ISkyEvent.java
  65. 8 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/model/RoomInfo.java
  66. 18 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/model/UserInfo.java
  67. 27 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/render/ProxyVideoSink.java
  68. 171 0
      wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/render/VideoFileRenderer.java
  69. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_audio_answer.png
  70. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_audio_answer_hover.png
  71. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_camera.png
  72. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_camera_hover.png
  73. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_default_header.png
  74. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_float_audio.png
  75. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_handfree.png
  76. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_handfree_hover.png
  77. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_hang_up.png
  78. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_hang_up_hover.png
  79. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_minimize.png
  80. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_mute.png
  81. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_mute_hover.png
  82. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_phone.png
  83. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_trans_audio.png
  84. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_video_answer.png
  85. BIN
      wdklRTC/src/main/res/drawable-xhdpi/av_video_answer_hover.png
  86. 20 0
      wdklRTC/src/main/res/drawable-xhdpi/bg_btn_white.xml
  87. 10 0
      wdklRTC/src/main/res/drawable/av_audio_answer_selector.xml
  88. 16 0
      wdklRTC/src/main/res/drawable/av_float_bg.xml
  89. 10 0
      wdklRTC/src/main/res/drawable/av_hangup_selector.xml
  90. 5 0
      wdklRTC/src/main/res/drawable/av_mute_selector.xml
  91. 5 0
      wdklRTC/src/main/res/drawable/av_speaker_selector.xml
  92. 10 0
      wdklRTC/src/main/res/drawable/av_switch_camera_selector.xml
  93. 10 0
      wdklRTC/src/main/res/drawable/av_video_answer_selector.xml
  94. 9 0
      wdklRTC/src/main/res/drawable/ic_dashboard_black_24dp.xml
  95. 9 0
      wdklRTC/src/main/res/drawable/ic_home_black_24dp.xml
  96. 9 0
      wdklRTC/src/main/res/drawable/ic_notifications_black_24dp.xml
  97. 47 0
      wdklRTC/src/main/res/layout/activity_launcher.xml
  98. 32 0
      wdklRTC/src/main/res/layout/activity_main.xml
  99. 21 0
      wdklRTC/src/main/res/layout/activity_multi_call.xml
  100. 0 0
      wdklRTC/src/main/res/layout/activity_single_call.xml

+ 1 - 0
home/build.gradle

@@ -119,6 +119,7 @@ dependencies {
 
     compile project(':starRTC')
     compile project(':AmDemo_R')
+    compile project(':wdklRTC')
 }
 
 /**

+ 1 - 1
settings.gradle

@@ -1 +1 @@
-include ':app', ':common', ':welcome', ':home', ':resource', ':middleware', ':shop', ':setting', ':extra', ':hello', ':starRTC', ':AmDemo_R'
+include ':app', ':common', ':welcome', ':home', ':resource', ':middleware', ':shop', ':setting', ':extra', ':hello', ':starRTC', ':AmDemo_R',':wdklRTC'

+ 5 - 0
wdklRTC/.gitignore

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

+ 68 - 0
wdklRTC/build.gradle

@@ -0,0 +1,68 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion rootProject.ext.target_sdk_version
+    defaultConfig {
+//        applicationId "com.wdkl.webrtc"
+        minSdkVersion rootProject.ext.min_sdk_version
+        targetSdkVersion rootProject.ext.target_sdk_version
+        versionCode rootProject.ext.app_version_code
+        versionName rootProject.ext.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 {
+//            applicationIdSuffix ".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 'com.google.android.material:material:1.1.0'
+    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'
+
+    // 内存泄漏检测
+    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
+    // java
+    implementation 'org.java-websocket:Java-WebSocket:1.4.0'
+    implementation 'com.alibaba:fastjson:1.2.23'
+    //强大的弹窗库
+    implementation 'com.lxj:xpopup:2.2.0'
+    //eventbus
+    implementation 'org.greenrobot:eventbus:3.1.1'
+
+    implementation 'com.blankj:utilcodex:1.30.5'
+
+    //通知提示弹出库
+    api 'com.tapadoo.android:alerter:6.2.1'
+
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    api group: 'org.webrtc', name: 'google-webrtc', version: '1.0.32006'
+}

BIN
wdklRTC/dds.jks


+ 25 - 0
wdklRTC/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

+ 84 - 0
wdklRTC/src/main/AndroidManifest.xml

@@ -0,0 +1,84 @@
+<?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.rtclib">
+    <!-- 设置视频直播权限 -->
+    <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:name="com.wdkl.webrtc.demo.App"
+        android:allowBackup="false"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme"
+        android:usesCleartextTraffic="true"
+        tools:ignore="UnusedAttribute">
+
+        <activity
+            android:name="com.wdkl.webrtc.demo.LauncherActivity"
+            android:label="@string/app_name"
+            android:theme="@style/AppTheme.NoActionBar">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <action android:name="android.intent.action.VIEW" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+
+        <activity android:name="com.wdkl.webrtc.demo.core.MainActivity" />
+
+        <!-- ======================java====================== -->
+        <activity
+            android:name="com.wdkl.webrtc.demo.core.voip.CallSingleActivity"
+            android:screenOrientation="portrait"
+            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.webrtc.demo.core.voip.CallMultiActivity"
+            android:screenOrientation="portrait"
+            android:showOnLockScreen="true"
+            android:theme="@style/AppTheme.NoActionBar" />
+
+        <receiver android:name="com.wdkl.webrtc.demo.core.voip.VoipReceiver">
+            <intent-filter>
+                <action android:name="${applicationId}.voip.Receiver" />
+            </intent-filter>
+        </receiver>
+
+        <!-- 悬浮窗 -->
+        <service
+            android:name="com.wdkl.webrtc.demo.core.voip.FloatingVoipService"
+            android:enabled="true"
+            android:exported="true" />
+    </application>
+
+</manifest>

+ 60 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/App.java

@@ -0,0 +1,60 @@
+package com.wdkl.webrtc.demo;
+
+import android.app.Application;
+
+import com.wdkl.webrtc.demo.core.util.CrashHandler;
+import com.wdkl.webrtc.demo.core.voip.VoipEvent;
+import com.wdkl.webrtc.demo.net.HttpRequestPresenter;
+import com.wdkl.webrtc.demo.net.urlconn.UrlConnRequest;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+
+/**
+ * Created by Allen.Fu
+ */
+public class App extends Application {
+
+    private static App app;
+    private String username = "";
+    private String roomId = "";
+    private String otherUserId = "";
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        app = this;
+        Thread.setDefaultUncaughtExceptionHandler(new CrashHandler());
+        // 初始化网络请求
+        HttpRequestPresenter.init(new UrlConnRequest());
+        // 初始化信令
+        SkyEngineKit.init(new VoipEvent());
+
+    }
+
+    public static App getInstance() {
+        return app;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getRoomId() {
+        return roomId;
+    }
+
+    public void setRoomId(String roomId) {
+        this.roomId = roomId;
+    }
+
+    public String getOtherUserId() {
+        return otherUserId;
+    }
+
+    public void setOtherUserId(String otherUserId) {
+        this.otherUserId = otherUserId;
+    }
+}

+ 87 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/LauncherActivity.java

@@ -0,0 +1,87 @@
+package com.wdkl.webrtc.demo;
+
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import androidx.appcompat.widget.Toolbar;
+
+import com.wdkl.webrtc.demo.App;
+import com.wdkl.webrtc.demo.core.MainActivity;
+import com.wdkl.webrtc.demo.core.base.BaseActivity;
+import com.wdkl.webrtc.demo.core.consts.Urls;
+import com.wdkl.webrtc.demo.core.socket.IUserState;
+import com.wdkl.webrtc.demo.core.socket.SocketManager;
+import com.wdkl.webrtc.rtclib.R;
+
+public class LauncherActivity extends BaseActivity implements IUserState {
+    private Toolbar toolbar;
+    private EditText etUser;
+    private Button button8;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_launcher);
+
+        initView();
+
+        if (SocketManager.getInstance().getUserState() == 1) {
+            startActivity(new Intent(this, MainActivity.class));
+            finish();
+        }
+    }
+
+    private void initView() {
+        toolbar = findViewById(R.id.toolbar);
+        setSupportActionBar(toolbar);
+        toolbar = findViewById(R.id.toolbar);
+        etUser = findViewById(R.id.et_user);
+        button8 = findViewById(R.id.button8);
+
+        etUser.setText(App.getInstance().getUsername());
+    }
+
+    public void java(View view) {
+        String username = etUser.getText().toString().trim();
+        if (TextUtils.isEmpty(username)) {
+            Toast.makeText(this, "please input your name", Toast.LENGTH_LONG).show();
+            return;
+        }
+
+        // 设置用户名
+        App.getInstance().setUsername(username);
+        // 添加登录回调
+        SocketManager.getInstance().addUserStateCallback(this);
+        // 连接socket:登录
+        SocketManager.getInstance().connect(Urls.WS, username, 0);
+
+
+    }
+
+    @Override
+    public void userLogin() {
+        startActivity(new Intent(this, MainActivity.class));
+        finish();
+    }
+
+    @Override
+    public void userLogout() {
+
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            finishAfterTransition();
+        } else {
+            super.onBackPressed();
+        }
+
+    }
+}

+ 97 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/MainActivity.java

@@ -0,0 +1,97 @@
+package com.wdkl.webrtc.demo.core;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+import androidx.navigation.ui.AppBarConfiguration;
+import androidx.navigation.ui.NavigationUI;
+
+import com.blankj.utilcode.util.LogUtils;
+import com.wdkl.webrtc.demo.App;
+import com.wdkl.webrtc.demo.LauncherActivity;
+import com.wdkl.webrtc.demo.core.base.BaseActivity;
+import com.wdkl.webrtc.demo.core.socket.IUserState;
+import com.wdkl.webrtc.demo.core.socket.SocketManager;
+import com.wdkl.webrtc.demo.core.voip.Utils;
+import com.wdkl.webrtc.demo.core.voip.VoipReceiver;
+import com.wdkl.webrtc.rtclib.R;
+import com.google.android.material.bottomnavigation.BottomNavigationView;
+
+/**
+ * 主界面
+ */
+public class MainActivity extends BaseActivity implements IUserState {
+    private static final String TAG = "MainActivity";
+    boolean isFromCall;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+        BottomNavigationView navView = findViewById(R.id.nav_view);
+        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
+        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
+                R.id.navigation_user, R.id.navigation_room, R.id.navigation_setting)
+                .build();
+        // 設置ActionBar跟随联动
+        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
+        // 设置Nav跟随联动
+        NavigationUI.setupWithNavController(navView, navController);
+        // 设置登录状态回调
+        SocketManager.getInstance().addUserStateCallback(this);
+        isFromCall = getIntent().getBooleanExtra("isFromCall", false);
+        LogUtils.dTag(TAG, "onCreate isFromCall = " + isFromCall);
+        if (isFromCall) { //无权限,来电申请权限会走这里
+            initCall();
+        }
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        LogUtils.dTag(TAG, "onStart isFromCall = " + isFromCall);
+    }
+
+    @Override
+    public void userLogin() {
+
+    }
+
+    private void initCall() {
+        //在前台了,发送广播 调起权限判断弹窗
+        Intent viop = new Intent();
+        Intent intent = getIntent();
+        viop.putExtra("room", intent.getStringExtra("room"));
+        viop.putExtra("audioOnly", intent.getBooleanExtra("audioOnly", false));
+        viop.putExtra("inviteId", intent.getStringExtra("inviteId"));
+        viop.putExtra("inviteUserName", intent.getStringExtra("inviteUserName"));
+//        viop.putExtra("msgId", intent.getLongExtra("msgId", 0));
+        viop.putExtra("userList", intent.getStringExtra("userList"));
+        viop.setAction(Utils.ACTION_VOIP_RECEIVER);
+        viop.setComponent(new ComponentName(App.getInstance().getPackageName(), VoipReceiver.class.getName()));
+        sendBroadcast(viop);
+    }
+
+    @Override
+    public void userLogout() {
+        if (!this.isFinishing()) {
+            Intent intent = new Intent(this, LauncherActivity.class);
+            startActivity(intent);
+            this.finish();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            finishAfterTransition();
+        } else {
+            super.onBackPressed();
+        }
+
+    }
+}

+ 23 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/base/BaseActivity.java

@@ -0,0 +1,23 @@
+package com.wdkl.webrtc.demo.core.base;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.wdkl.webrtc.demo.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();
+    }
+}

+ 26 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/consts/Urls.java

@@ -0,0 +1,26 @@
+package com.wdkl.webrtc.demo.core.consts;
+
+/**
+ * Created by Allen.Fu on 2020/4/19.
+ * Allen.Fu
+ */
+public class Urls {
+
+    //    private final static String IP = "192.168.2.111";
+    public final static String IP = "8.130.170.205:5000";
+
+    private final static String HOST = "http://" + IP + "/";
+
+    // 信令地址
+    public final static String WS  = "ws://" + IP + "/ws";
+
+    // 获取用户列表
+    public static String getUserList() {
+        return HOST + "userList";
+    }
+
+    // 获取房间列表
+    public static String getRoomList() {
+        return HOST + "roomList";
+    }
+}

+ 48 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/socket/IEvent.java

@@ -0,0 +1,48 @@
+package com.wdkl.webrtc.demo.core.socket;
+
+/**
+ * Created by Allen.Fu on 2019/7/26.
+ * Allen.Fu
+ */
+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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/socket/IUserState.java

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

+ 500 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/socket/MyWebSocket.java

@@ -0,0 +1,500 @@
+package com.wdkl.webrtc.demo.core.socket;
+
+import android.annotation.SuppressLint;
+import android.util.Log;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.wdkl.webrtc.demo.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 Allen.Fu 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;
+    }
+
+    @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;
+    }
+
+    // ---------------------------------------处理接收消息-------------------------------------
+
+    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];
+        }
+    }
+
+}

+ 358 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/socket/SocketManager.java

@@ -0,0 +1,358 @@
+package com.wdkl.webrtc.demo.core.socket;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.wdkl.webrtc.demo.App;
+import com.wdkl.webrtc.demo.core.voip.Utils;
+import com.wdkl.webrtc.demo.core.voip.VoipReceiver;
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.EnumType;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+
+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 Allen.Fu 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 final Handler handler = new Handler(Looper.getMainLooper());
+
+    private SocketManager() {
+
+    }
+
+    private static class Holder {
+        private static final SocketManager socketManager = new SocketManager();
+    }
+
+    public static SocketManager getInstance() {
+        return Holder.socketManager;
+    }
+
+    public void connect(String url, String userId, int device) {
+        System.out.println(">>>>>>>>>>>>>" + url);
+        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;
+        }
+
+    }
+
+    @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);
+        }
+    }
+
+    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);
+        }
+    }
+
+    public void sendCancel(String mRoomId, List<String> userIds) {
+        if (webSocket != null) {
+            webSocket.sendCancel(mRoomId, myId, userIds);
+        }
+    }
+
+    public void sendJoin(String room) {
+        if (webSocket != null) {
+            webSocket.sendJoin(room, myId);
+        }
+    }
+
+    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(App.getInstance().getPackageName(), VoipReceiver.class.getName()));
+        // 发送广播
+        App.getInstance().sendBroadcast(intent);
+
+    }
+
+    @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);
+            }
+        });
+    }
+
+    @Override
+    public void reConnect() {
+        handler.post(() -> {
+            webSocket.reconnect();
+        });
+    }
+    //===========================================================================================
+
+
+    public int getUserState() {
+        return userState;
+    }
+
+    private WeakReference<IUserState> iUserState;
+
+    public void addUserStateCallback(IUserState userState) {
+        iUserState = new WeakReference<>(userState);
+    }
+
+}

+ 35 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/event/MsgEvent.java

@@ -0,0 +1,35 @@
+package com.wdkl.webrtc.demo.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;
+    }
+}
+

+ 153 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/room/RoomFragment.java

@@ -0,0 +1,153 @@
+package com.wdkl.webrtc.demo.core.ui.room;
+
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import com.wdkl.webrtc.demo.core.voip.CallMultiActivity;
+import com.wdkl.webrtc.rtclib.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+
+public class RoomFragment extends Fragment {
+
+    private RoomViewModel roomViewModel;
+    private RecyclerView list;
+    private List<RoomInfo> datas;
+    private RoomAdapter adapter;
+    private SwipeRefreshLayout refreshLayout;
+    private TextView no_data;
+
+    public View onCreateView(@NonNull LayoutInflater inflater,
+                             ViewGroup container, Bundle savedInstanceState) {
+
+        roomViewModel = new ViewModelProvider(requireActivity()).get(RoomViewModel.class);
+        View root = inflater.inflate(R.layout.fragment_room, container, false);
+        initView(root);
+        initData();
+
+        return root;
+    }
+
+
+    private void initView(View root) {
+        setHasOptionsMenu(true);
+        list = root.findViewById(R.id.list);
+        refreshLayout = root.findViewById(R.id.swipe);
+        no_data = root.findViewById(R.id.no_data);
+    }
+
+    private void initData() {
+        adapter = new RoomAdapter();
+        datas = new ArrayList<>();
+        list.setAdapter(adapter);
+        list.setLayoutManager(new LinearLayoutManager(getContext()));
+
+        roomViewModel.getRoomList().observe(getViewLifecycleOwner(), roomInfos -> {
+            if (roomInfos.size() > 0) {
+                no_data.setVisibility(View.GONE);
+            } else {
+                no_data.setVisibility(View.VISIBLE);
+            }
+            datas.clear();
+            datas.addAll(roomInfos);
+            adapter.notifyDataSetChanged();
+            refreshLayout.setRefreshing(false);
+        });
+
+        refreshLayout.setOnRefreshListener(() -> roomViewModel.loadRooms());
+    }
+
+
+    @Override
+    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.menu_room, menu);
+    }
+
+
+    @Override
+    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+        if (item.getItemId() == R.id.action_create) {
+            createRoom();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+
+    // 创建房间
+    private void createRoom() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        builder.setMessage("自动创建一个房间并进入房间");
+        builder.setPositiveButton("确定", (dialog, which) -> {
+            // 创建一个房间并进入
+            CallMultiActivity.openActivity(getActivity(),
+                    "room-" + UUID.randomUUID().toString().substring(0, 16), true);
+
+
+        }).setNegativeButton("取消", (dialog, which) -> dialog.dismiss());
+        AlertDialog dialog = builder.create();
+        dialog.show();
+
+    }
+
+
+    private class RoomAdapter extends RecyclerView.Adapter<RoomFragment.Holder> {
+
+        @NonNull
+        @Override
+        public RoomFragment.Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(getContext()).inflate(R.layout.item_rooms, parent, false);
+            return new RoomFragment.Holder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull RoomFragment.Holder holder, int position) {
+            RoomInfo roomInfo = datas.get(position);
+            holder.text.setText(roomInfo.getRoomId());
+            holder.item_join_room.setOnClickListener(v -> {
+                CallMultiActivity.openActivity(getActivity(), roomInfo.getRoomId(), false);
+            });
+        }
+
+
+        @Override
+        public int getItemCount() {
+            return datas.size();
+        }
+
+
+    }
+
+    private static class Holder extends RecyclerView.ViewHolder {
+
+        private final TextView text;
+        private final Button item_join_room;
+
+        Holder(View itemView) {
+            super(itemView);
+            text = itemView.findViewById(R.id.item_user_name);
+            item_join_room = itemView.findViewById(R.id.item_join_room);
+        }
+    }
+
+
+}

+ 46 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/room/RoomInfo.java

@@ -0,0 +1,46 @@
+package com.wdkl.webrtc.demo.core.ui.room;
+
+/**
+ * Created by Allen.Fu on 2020/5/1.
+ * Allen.Fu
+ */
+public class RoomInfo {
+
+    private String roomId;
+    private String userId;
+    private int maxSize;
+    private int currentSize;
+
+
+    public String getRoomId() {
+        return roomId;
+    }
+
+    public void setRoomId(String roomId) {
+        this.roomId = roomId;
+    }
+
+    public int getMaxSize() {
+        return maxSize;
+    }
+
+    public void setMaxSize(int maxSize) {
+        this.maxSize = maxSize;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public int getCurrentSize() {
+        return currentSize;
+    }
+
+    public void setCurrentSize(int currentSize) {
+        this.currentSize = currentSize;
+    }
+}

+ 50 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/room/RoomViewModel.java

@@ -0,0 +1,50 @@
+package com.wdkl.webrtc.demo.core.ui.room;
+
+import android.util.Log;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.alibaba.fastjson.JSON;
+import com.wdkl.webrtc.demo.core.consts.Urls;
+import com.wdkl.webrtc.demo.net.HttpRequestPresenter;
+import com.wdkl.webrtc.demo.net.ICallback;
+
+import java.util.List;
+
+public class RoomViewModel extends ViewModel {
+
+    private MutableLiveData<List<RoomInfo>> mList;
+    private Thread thread;
+
+    public RoomViewModel() {
+    }
+
+    public MutableLiveData<List<RoomInfo>> getRoomList() {
+        if (mList == null) {
+            mList = new MutableLiveData<>();
+            loadRooms();
+        }
+        return mList;
+    }
+
+    public void loadRooms() {
+        thread = new Thread(() -> {
+            String url = Urls.getRoomList();
+            HttpRequestPresenter.getInstance().get(url, null, new ICallback() {
+                @Override
+                public void onSuccess(String result) {
+                    Log.d("dds_test", result);
+                    List<RoomInfo> roomInfos = JSON.parseArray(result, RoomInfo.class);
+                    mList.postValue(roomInfos);
+                }
+
+                @Override
+                public void onFailure(int code, Throwable t) {
+                    Log.d("dds_test", "code:" + code + ",msg:" + t.toString());
+                }
+            });
+        });
+        thread.start();
+    }
+}

+ 37 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/setting/SettingFragment.java

@@ -0,0 +1,37 @@
+package com.wdkl.webrtc.demo.core.ui.setting;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.wdkl.webrtc.demo.core.socket.SocketManager;
+import com.wdkl.webrtc.rtclib.R;
+
+
+public class SettingFragment extends Fragment {
+
+    private SettingViewModel notificationsViewModel;
+    private Button button;
+
+    public View onCreateView(@NonNull LayoutInflater inflater,
+                             ViewGroup container, Bundle savedInstanceState) {
+        notificationsViewModel = new ViewModelProvider(requireActivity()).get(SettingViewModel.class);
+        View root = inflater.inflate(R.layout.fragment_setting, container, false);
+        final TextView textView = root.findViewById(R.id.text_notifications);
+        button = root.findViewById(R.id.exit);
+        button.setOnClickListener(view -> {
+            SocketManager.getInstance().unConnect();
+        });
+        notificationsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
+        return root;
+    }
+
+
+}

+ 19 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/setting/SettingViewModel.java

@@ -0,0 +1,19 @@
+package com.wdkl.webrtc.demo.core.ui.setting;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+public class SettingViewModel extends ViewModel {
+
+    private MutableLiveData<String> mText;
+
+    public SettingViewModel() {
+        mText = new MutableLiveData<>();
+        mText.setValue("This is setting fragment");
+    }
+
+    public LiveData<String> getText() {
+        return mText;
+    }
+}

+ 42 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/user/UserBean.java

@@ -0,0 +1,42 @@
+package com.wdkl.webrtc.demo.core.ui.user;
+
+import android.text.TextUtils;
+
+/**
+ * Created by Allen.Fu 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;
+    }
+
+
+}

+ 148 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/user/UserListFragment.java

@@ -0,0 +1,148 @@
+package com.wdkl.webrtc.demo.core.ui.user;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+
+import com.wdkl.webrtc.demo.App;
+import com.wdkl.webrtc.demo.core.voip.CallSingleActivity;
+import com.wdkl.webrtc.demo.core.voip.VoipEvent;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class UserListFragment extends Fragment {
+
+    private UserListViewModel homeViewModel;
+    private RecyclerView list;
+    private List<UserBean> datas;
+    private UserAdapter adapter;
+    private SwipeRefreshLayout refreshLayout;
+    private TextView no_data;
+
+    public View onCreateView(@NonNull LayoutInflater inflater,
+                             ViewGroup container, Bundle savedInstanceState) {
+        homeViewModel = new ViewModelProvider(requireActivity()).get(UserListViewModel.class);
+        View root = inflater.inflate(R.layout.fragment_home, container, false);
+        initView(root);
+        initData();
+        return root;
+    }
+
+    private void initView(View root) {
+        list = root.findViewById(R.id.list);
+        refreshLayout = root.findViewById(R.id.swipe);
+        no_data = root.findViewById(R.id.no_data);
+    }
+
+
+    private void initData() {
+        adapter = new UserAdapter();
+        datas = new ArrayList<>();
+        list.setAdapter(adapter);
+        list.setLayoutManager(new LinearLayoutManager(getContext()));
+        list.setHasFixedSize(true);
+
+        homeViewModel.getUserList().observe(getViewLifecycleOwner(), userBeans -> {
+            if (userBeans.size() == 0) {
+                no_data.setVisibility(View.VISIBLE);
+            } else {
+                no_data.setVisibility(View.GONE);
+            }
+            datas.clear();
+            datas.addAll(userBeans);
+            adapter.notifyDataSetChanged();
+            refreshLayout.setRefreshing(false);
+        });
+
+        refreshLayout.setOnRefreshListener(() -> {
+            homeViewModel.loadUsers();
+        });
+
+    }
+
+
+    @Override
+    public void onPause() {
+        super.onPause();
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+    }
+
+
+    @Override
+    public void onHiddenChanged(boolean hidden) {
+        super.onHiddenChanged(hidden);
+        refreshLayout.setRefreshing(false);
+    }
+
+    private class UserAdapter extends RecyclerView.Adapter<Holder> {
+
+        @NonNull
+        @Override
+        public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+            View view = LayoutInflater.from(getContext()).inflate(R.layout.item_users, parent, false);
+            return new Holder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(@NonNull Holder holder, int position) {
+            UserBean userBean = datas.get(position);
+            holder.text.setText(userBean.getNickName());
+            if (App.getInstance().getUsername().equals(userBean.getUserId())) {
+                holder.item_call_audio.setVisibility(View.GONE);
+                holder.item_call_video.setVisibility(View.GONE);
+            } else {
+                holder.item_call_audio.setVisibility(View.VISIBLE);
+                holder.item_call_video.setVisibility(View.VISIBLE);
+            }
+            holder.item_call_video.setOnClickListener(view -> {
+                CallSingleActivity.openActivity(getContext(), userBean.getUserId(), true, userBean.getNickName(), false, false);
+
+            });
+            holder.item_call_audio.setOnClickListener(view -> {
+                SkyEngineKit.init(new VoipEvent());
+                CallSingleActivity.openActivity(getContext(), userBean.getUserId(), true, userBean.getNickName(), true, false);
+            });
+        }
+
+
+        @Override
+        public int getItemCount() {
+            return datas.size();
+        }
+
+
+    }
+
+    private class Holder extends RecyclerView.ViewHolder {
+
+        private final TextView text;
+        private Button item_call_audio;
+        private Button item_call_video;
+
+        Holder(View itemView) {
+            super(itemView);
+            text = itemView.findViewById(R.id.item_user_name);
+            item_call_audio = itemView.findViewById(R.id.item_call_audio);
+            item_call_video = itemView.findViewById(R.id.item_call_video);
+        }
+    }
+
+}

+ 53 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/ui/user/UserListViewModel.java

@@ -0,0 +1,53 @@
+package com.wdkl.webrtc.demo.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.webrtc.demo.core.consts.Urls;
+import com.wdkl.webrtc.demo.net.HttpRequestPresenter;
+import com.wdkl.webrtc.demo.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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/ActivityStackManager.java

@@ -0,0 +1,131 @@
+package com.wdkl.webrtc.demo.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());
+    }
+
+}

+ 53 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/CrashHandler.java

@@ -0,0 +1,53 @@
+package com.wdkl.webrtc.demo.core.util;
+
+import android.content.Intent;
+import android.util.Log;
+
+import com.wdkl.webrtc.demo.App;
+import com.wdkl.webrtc.demo.LauncherActivity;
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.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";
+
+    @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(App.getInstance(), LauncherActivity.class);
+        i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+        App.getInstance().startActivity(i);
+    }
+
+
+}

+ 127 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/OSUtils.java

@@ -0,0 +1,127 @@
+package com.wdkl.webrtc.demo.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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/StringUtil.java

@@ -0,0 +1,25 @@
+package com.wdkl.webrtc.demo.core.util;
+
+import java.util.List;
+
+/**
+ * Created by Allen.Fu on 2020/5/4.
+ * Allen.Fu
+ */
+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 "";
+    }
+}

+ 53 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/util/Utils.java

@@ -0,0 +1,53 @@
+package com.wdkl.webrtc.demo.core.util;
+
+import android.app.ActivityManager;
+import android.app.Application;
+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 com.wdkl.webrtc.demo.App;
+
+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() {
+        Resources resources = App.getInstance().getResources();
+        int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
+        return resources.getDimensionPixelSize(resourceId);
+    }
+
+
+    public static boolean isAppRunningForeground() {
+        ActivityManager activityManager =
+                (ActivityManager) App.getInstance().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(App.getInstance().getApplicationInfo().processName))
+                    return true;
+            }
+        }
+        return false;
+    }
+}

+ 187 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/AsyncPlayer.java

@@ -0,0 +1,187 @@
+package com.wdkl.webrtc.demo.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;
+        Uri uri;
+        boolean looping;
+        int stream;
+        long requestTime;
+
+        public String toString() {
+            return "{ code=" + code + " looping=" + looping + " stream=" + stream + " uri=" + uri + " }";
+        }
+    }
+
+    private final LinkedList mCmdQueue = new LinkedList();
+
+    private void startSound(Command cmd) {
+
+        try {
+            MediaPlayer player = new MediaPlayer();
+            player.setAudioStreamType(cmd.stream);
+            player.setDataSource(cmd.context, cmd.uri);
+            player.setLooping(cmd.looping);
+            player.prepare();
+            player.start();
+            if (mPlayer != null) {
+                mPlayer.release();
+            }
+            mPlayer = player;
+        } catch (IOException e) {
+            Log.w(mTag, "error loading sound for " + cmd.uri, e);
+        } catch (IllegalStateException e) {
+            Log.w(mTag, "IllegalStateException (content provider died?) " + cmd.uri, e);
+        }
+    }
+
+    private final class Thread extends java.lang.Thread {
+        Thread() {
+            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.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, Uri uri, boolean looping, int stream) {
+        Command cmd = new Command();
+        cmd.requestTime = SystemClock.uptimeMillis();
+        cmd.code = PLAY;
+        cmd.context = context;
+        cmd.uri = uri;
+        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;
+            }
+        }
+    }
+
+    private void enqueueLocked(Command cmd) {
+        mCmdQueue.add(cmd);
+        if (mThread == null) {
+            acquireWakeLock();
+            mThread = new Thread();
+            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();
+        }
+    }
+}

+ 121 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/CallForegroundNotification.java

@@ -0,0 +1,121 @@
+package com.wdkl.webrtc.demo.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.webrtc.demo.core.util.ActivityStackManager;
+import com.wdkl.webrtc.rtclib.R;
+
+import java.util.Random;
+
+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();
+    }
+
+
+}

+ 195 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/CallMultiActivity.java

@@ -0,0 +1,195 @@
+package com.wdkl.webrtc.demo.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.view.View;
+import android.view.WindowManager;
+import android.widget.ImageView;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import com.wdkl.webrtc.demo.core.base.BaseActivity;
+import com.wdkl.webrtc.demo.permission.Permissions;
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.EnumType;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.except.NotInitializedException;
+import com.wdkl.webrtc.rtclib.R;
+
+import java.util.UUID;
+
+/**
+ * Created by Allen.Fu 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;
+
+
+    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());
+        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-" + UUID.randomUUID().toString().substring(0, 16));
+        } else {
+            // 加入房间
+            gEngineKit.joinRoom(this, room);
+        }
+
+
+        CallSession session = gEngineKit.getCurrentSession();
+        if (session == null) {
+            this.finish();
+        } else {
+            session.setSessionCallback(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));
+    }
+
+    @Override
+    public void didError(String var1) {
+        finish();
+    }
+
+    @Override
+    public void didDisconnected(String userId) {
+
+    }
+
+    @Override
+    public void onClick(View v) {
+        switch (v.getId()) {
+            case R.id.meetingHangupImageView:
+                handleHangup();
+                break;
+        }
+
+    }
+
+    // 处理挂断事件
+    private void handleHangup() {
+        SkyEngineKit.Instance().leaveRoom();
+        this.finish();
+    }
+}

+ 339 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/CallSingleActivity.java

@@ -0,0 +1,339 @@
+package com.wdkl.webrtc.demo.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.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import androidx.fragment.app.FragmentManager;
+
+import com.wdkl.webrtc.demo.App;
+import com.wdkl.webrtc.demo.core.base.BaseActivity;
+import com.wdkl.webrtc.demo.permission.Permissions;
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.EnumType;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.except.NotInitializedException;
+import com.wdkl.webrtc.rtclib.R;
+
+import java.util.UUID;
+
+
+/**
+ * Created by Allen.Fu 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, false);
+                } 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, 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();
+    }
+}

+ 439 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/FloatingVoipService.java

@@ -0,0 +1,439 @@
+package com.wdkl.webrtc.demo.core.voip;
+
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.BounceInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+
+import com.blankj.utilcode.util.BarUtils;
+import com.blankj.utilcode.util.LogUtils;
+import com.blankj.utilcode.util.ScreenUtils;
+import com.blankj.utilcode.util.SizeUtils;
+import com.wdkl.webrtc.demo.core.ui.event.MsgEvent;
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.CallSession.CallSessionCallback;
+import com.wdkl.webrtc.rtclib.EnumType.CallEndReason;
+import com.wdkl.webrtc.rtclib.EnumType.CallState;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.BuildConfig;
+import com.wdkl.webrtc.rtclib.R;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * 悬浮窗界面
+ */
+public class FloatingVoipService extends Service {
+    private CallSession session;
+    private Intent resumeActivityIntent;
+    private Handler handler = new Handler();
+    private WindowManager wm;
+    private View view;
+    private WindowManager.LayoutParams params;
+    private int touchSlop = 0;
+    private LinearLayout audioView;
+    private FrameLayout videoView;
+    private int margin = 0;
+    final int statusBarHeight = BarUtils.getStatusBarHeight();
+    private int screenWidth = 0;
+    private int screenHeight = 0;
+    private PowerManager.WakeLock wakeLock;
+    HeadsetPlugReceiver headsetPlugReceiver;
+    private final static String TAG = "FloatingVoipService";
+    private static boolean isStarted = false;
+    private final static int NOTIFICATION_ID = 1;
+    private ViewGroup floatingView;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        touchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
+        if (!EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().register(this);
+        }
+        headsetPlugReceiver = new HeadsetPlugReceiver();
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_HEADSET_PLUG);
+        registerReceiver(headsetPlugReceiver, filter);
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (isStarted) {
+            return START_NOT_STICKY;
+        }
+        isStarted = true;
+        session = SkyEngineKit.Instance().getCurrentSession();
+        if (session == null || CallState.Idle.equals(session.getState())) {
+            stopSelf();
+        }
+        resumeActivityIntent = new Intent(this, CallSingleActivity.class);
+        resumeActivityIntent = new Intent(this, CallSingleActivity.class);
+        resumeActivityIntent.putExtra(CallSingleActivity.EXTRA_FROM_FLOATING_VIEW, true);
+        resumeActivityIntent.putExtra(CallSingleActivity.EXTRA_MO, intent.getBooleanExtra(CallSingleActivity.EXTRA_MO, false));
+        resumeActivityIntent.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, intent.getBooleanExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, false));
+        resumeActivityIntent.putExtra(CallSingleActivity.EXTRA_TARGET, intent.getStringExtra(CallSingleActivity.EXTRA_TARGET));
+        resumeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, resumeActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        String channelId = "";
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            channelId = BuildConfig.APPLICATION_ID + ".voip";
+            String channelName = "voip";
+            NotificationChannel chan = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH);
+            chan.setLightColor(Color.BLUE);
+            chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
+            NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+            manager.createNotificationChannel(chan);
+        }
+        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId);
+        builder.setSmallIcon(R.mipmap.ic_launcher)
+                .setContentTitle("通话中...")
+                .setContentIntent(pendingIntent)
+                .setOngoing(true)
+                .build();
+        startForeground(NOTIFICATION_ID, builder.build());
+        try {
+            showFloatingWindow();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return START_NOT_STICKY;
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    public void onEvent(MsgEvent<Object> messageEvent) {
+        int code = messageEvent.getCode();
+        if (code == MsgEvent.CODE_ON_CALL_ENDED) {
+            hideFloatBox();
+        }
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().unregister(this);
+        }
+        unregisterReceiver(headsetPlugReceiver);  //注销监听
+        releaseWakeLock();
+        super.onDestroy();
+        super.onDestroy();
+        try {
+            wm.removeView(view);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        isStarted = false;
+    }
+
+    private void showFloatingWindow() {
+        wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+        params = new WindowManager.LayoutParams();
+        margin = SizeUtils.dp2px(10f);
+        screenWidth = ScreenUtils.getScreenWidth();
+        screenHeight = ScreenUtils.getScreenHeight();
+        int type;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+        } else {
+            type = WindowManager.LayoutParams.TYPE_PHONE;
+        }
+        params.type = type;
+        params.flags = WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+
+        params.format = PixelFormat.TRANSLUCENT;
+        params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+        params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+        params.gravity = Gravity.END | Gravity.TOP;
+        view = LayoutInflater.from(this).inflate(R.layout.av_voip_float_view, null);
+        view.setOnTouchListener(onTouchListener);
+        params.x = margin;
+        params.y = statusBarHeight;
+        wm.addView(view, params);
+        if (session.isAudioOnly()) {
+            showAudioInfo();
+        } else {
+            showVideoInfo();
+        }
+        if (session == null) return;
+        session.setSessionCallback(new CallSessionCallback() {
+            @Override
+            public void didCallEndWithReason(CallEndReason var1) {
+                Log.d(TAG, "didCallEndWithReason");
+                hideFloatBox();
+            }
+
+            @Override
+            public void didChangeState(CallState var1) {
+
+            }
+
+            @Override
+            public void didChangeMode(boolean isAudioOnly) {
+                handler.post(() -> showAudioInfo());
+            }
+
+            @Override
+            public void didCreateLocalVideoTrack() {
+
+            }
+
+            @Override
+            public void didReceiveRemoteVideoTrack(String userId) {
+
+            }
+
+            @Override
+            public void didUserLeave(String userId) {
+                hideFloatBox();
+            }
+
+            @Override
+            public void didError(String error) {
+                hideFloatBox();
+            }
+
+            @Override
+            public void didDisconnected(String userId) {
+                hideFloatBox();
+            }
+        });
+
+    }
+
+    private void hideFloatBox() {
+        stopSelf();
+    }
+
+    private View.OnTouchListener onTouchListener = new View.OnTouchListener() {
+        int startX = 0;
+        int startY = 0;//起始点 = 0
+        boolean isPerformClick = false;//是否点击
+        int finalMoveX = 0;//最后通过动画将v的X轴坐标移动到finalMoveX
+
+
+        @SuppressLint("ClickableViewAccessibility")
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            Log.d("click", "onTouch: " + event.getAction());
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN: {
+                    startX = (int) event.getX();
+                    startY = (int) event.getY();
+                    isPerformClick = true;
+                    return true;
+                }
+                case MotionEvent.ACTION_MOVE: {
+                    //判断是CLICK还是MOVE
+                    //只要移动过,就认为不是点击
+                    if (Math.abs(startX - event.getX()) >= touchSlop || Math.abs(startY - event.getY()) >= touchSlop) {
+                        isPerformClick = false;
+                    }
+//                    LogUtil.d(TAG, "event.rawX = " + event.rawX + "; startX = " + startX)
+                    params.x = screenWidth - (int) (event.getRawX() - startX) - view.getWidth();
+                    //这里修复了刚开始移动的时候,悬浮窗的y坐标是不正确的,要减去状态栏的高度
+                    params.y = (int) (event.getRawY() - startY - statusBarHeight);
+                    if (params.x < margin) params.x = margin;
+                    if (params.x > screenWidth - margin) params.x = screenWidth - margin;
+
+                    if (params.y + view.getHeight() + statusBarHeight > screenHeight - margin)
+                        params.y =
+                                screenHeight - statusBarHeight - view.getHeight();
+
+//                    LogUtil.d(TAG, "x---->" + params.x)
+//                    LogUtil.d(TAG, "y---->" + params.y)
+                    updateViewLayout(); //更新v 的位置
+                    return true;
+                }
+                case MotionEvent.ACTION_UP: {
+                    if (isPerformClick) {
+                        v.performClick();
+                        clickToResume();
+                    }
+
+                    //判断v是在Window中的位置,以中间为界
+                    if (params.x + v.getMeasuredWidth() / 2 >= wm.getDefaultDisplay().getWidth() / 2) {
+                        finalMoveX = wm.getDefaultDisplay().getWidth() - v.getMeasuredWidth() - margin;
+                    } else {
+                        finalMoveX = margin;
+                    }
+                    stickToSide();
+                    return !isPerformClick;
+                }
+            }
+            return false;
+        }
+
+        private void stickToSide() {
+            ValueAnimator animator =
+                    ValueAnimator.ofInt(params.x, finalMoveX).setDuration(Math.abs(params.x - finalMoveX));
+            animator.setInterpolator(new BounceInterpolator());
+            animator.addUpdateListener(animation -> {
+                params.x = (int) animation.getAnimatedValue();
+                updateViewLayout();
+            });
+            animator.start();
+        }
+    };
+
+    private void clickToResume() {
+        startActivity(resumeActivityIntent);
+        hideFloatBox();
+    }
+
+    private void updateViewLayout() {
+        if (wm != null && view != null) {
+            wm.updateViewLayout(view, params);
+        }
+    }
+
+    private void refreshCallDurationInfo(TextView timeView) {
+        CallSession session = SkyEngineKit.Instance().getCurrentSession();
+        if (session == null || !session.isAudioOnly()) {
+            return;
+        }
+        long duration = (System.currentTimeMillis() - session.getStartTime()) / 1000;
+        if (duration >= 3600) {
+            timeView.setText(String.format(
+                    Locale.getDefault(), "%d:%02d:%02d",
+                    duration / 3600, duration % 3600 / 60, duration % 60
+            ));
+        } else {
+            timeView.setText(String.format(
+                    Locale.getDefault(), "%02d:%02d",
+                    duration % 3600 / 60, duration % 60
+            ));
+        }
+        handler.postDelayed(() -> refreshCallDurationInfo(timeView), 1000);
+    }
+
+
+    private ViewGroup getFloatingView() {
+        if (session == null) {
+            return null;
+        }
+        LogUtils.dTag(TAG, "getFloatingView session.isAudioOnly() = " + session.isAudioOnly());
+        if (session.isAudioOnly()) {
+            if (audioView == null) {
+                audioView = view.findViewById(R.id.audioLinearLayout);
+            }
+            return audioView;
+        } else {
+            if (videoView == null) {
+                videoView = view.findViewById(R.id.remoteVideoFrameLayout);
+            }
+            return videoView;
+        }
+    }
+
+    private void showAudioInfo() {
+        floatingView = Objects.requireNonNull(getFloatingView());
+        FrameLayout remoteVideoFrameLayout = view.findViewById(R.id.remoteVideoFrameLayout);
+        if (remoteVideoFrameLayout.getVisibility() == View.VISIBLE) {
+            remoteVideoFrameLayout.setVisibility(View.GONE);
+            wm.removeView(view);
+            wm.addView(view, params);
+        }
+        floatingView.setVisibility(View.VISIBLE);
+        TextView timeV = view.findViewById(R.id.durationTextView);
+        ImageView mediaIconV = view.findViewById(R.id.av_media_type);
+        mediaIconV.setImageResource(R.drawable.av_float_audio);
+        refreshCallDurationInfo(timeV);
+        releaseWakeLock();
+    }
+
+    private void showVideoInfo() {
+        newWakeLock();
+        view.findViewById(R.id.audioLinearLayout).setVisibility(View.GONE);
+        floatingView = Objects.requireNonNull(getFloatingView());
+        floatingView.setVisibility(View.VISIBLE);
+        View surfaceView = session.setupRemoteVideo(session.mTargetId, true);
+        if (surfaceView != null) {
+            if (surfaceView.getParent() != null) {
+                ((ViewGroup) (surfaceView.getParent())).removeView(surfaceView);
+            }
+            floatingView.removeAllViews();
+            floatingView.addView(surfaceView);
+        }
+    }
+
+
+    private void newWakeLock() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            Log.i(TAG, "setScreenOff: 熄灭屏幕");
+            if (wakeLock == null) {
+                wakeLock = ((PowerManager) getSystemService(POWER_SERVICE)).newWakeLock(
+                        PowerManager.PARTIAL_WAKE_LOCK,
+                        "$TAG:mywakelocktag"
+                );
+            }
+            wakeLock.acquire(1200 * 60 * 1000L /*20 hours*/);
+        }
+    }
+
+    private void releaseWakeLock() {
+        if (wakeLock != null) {
+            wakeLock.setReferenceCounted(false);
+            wakeLock.release();
+            wakeLock = null;
+        }
+    }
+
+    class HeadsetPlugReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.hasExtra("state")) {
+                if (session != null) {
+                    if (intent.getIntExtra("state", 0) == 0) { //拔出耳机
+                        session.toggleHeadset(false);
+                    } else if (intent.getIntExtra("state", 0) == 1) { //插入耳机
+                        session.toggleHeadset(true);
+                    }
+                }
+            }
+        }
+    }
+}

+ 104 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/FragmentAudio.java

@@ -0,0 +1,104 @@
+package com.wdkl.webrtc.demo.core.voip;
+
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.wdkl.webrtc.demo.App;
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.EnumType.CallState;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.R;
+
+/**
+ * Created by Allen.Fu on 2018/7/26.
+ * android_shuai@163.com
+ * 语音通话控制界面
+ */
+public class FragmentAudio extends SingleCallFragment implements View.OnClickListener {
+    private static final String TAG = "FragmentAudio";
+    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);
+        outgoingHangupImageView.setOnClickListener(this);
+        incomingHangupImageView.setOnClickListener(this);
+        acceptImageView.setOnClickListener(this);
+    }
+
+    @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);
+            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);
+                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结束了,消息发送不了
+        }
+    }
+}

+ 119 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/FragmentMeeting.java

@@ -0,0 +1,119 @@
+package com.wdkl.webrtc.demo.core.voip;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.EnumType;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.R;
+
+/**
+ * Created by Allen.Fu on 2020/5/24.
+ * Allen.Fu
+ */
+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) {
+
+    }
+
+
+}

+ 295 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/FragmentVideo.java

@@ -0,0 +1,295 @@
+package com.wdkl.webrtc.demo.core.voip;
+
+import android.content.Context;
+import android.os.Build;
+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 androidx.annotation.NonNull;
+
+import com.blankj.utilcode.util.BarUtils;
+import com.wdkl.webrtc.demo.core.util.OSUtils;
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.EnumType.CallState;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.R;
+
+import org.webrtc.SurfaceViewRenderer;
+
+/**
+ * Created by Allen.Fu 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 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);
+        outgoingHangupImageView.setOnClickListener(this);
+        incomingHangupImageView.setOnClickListener(this);
+
+        connectedHangupImageView.setOnClickListener(this);
+        acceptImageView.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);
+            });
+
+            pipRenderer.post(() -> {
+                FrameLayout.LayoutParams params2 = (FrameLayout.LayoutParams) pipRenderer.getLayoutParams();
+                params2.topMargin = (int) (BarUtils.getStatusBarHeight() * 1.2);
+                pipRenderer.setLayoutParams(params2);
+            });
+        }
+    }
+
+
+    @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);
+            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);
+                // 开启计时器
+                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.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();
+            }
+        }
+    }
+
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        fullscreenRenderer.removeAllViews();
+        pipRenderer.removeAllViews();
+    }
+}

+ 73 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/NineGridView.java

@@ -0,0 +1,73 @@
+package com.wdkl.webrtc.demo.core.voip;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.GridLayout;
+
+import com.wdkl.webrtc.rtclib.R;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by Allen.Fu on 2020/7/5.
+ * Allen.Fu
+ */
+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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/RomUtil.java

@@ -0,0 +1,153 @@
+package com.wdkl.webrtc.demo.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 Allen.Fu 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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/SettingsCompat.java

@@ -0,0 +1,265 @@
+package com.wdkl.webrtc.demo.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 Allen.Fu 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);
+        }
+    }
+}

+ 312 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/SingleCallFragment.java

@@ -0,0 +1,312 @@
+package com.wdkl.webrtc.demo.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.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 androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.wdkl.webrtc.demo.core.ui.event.MsgEvent;
+import com.wdkl.webrtc.rtclib.CallSession;
+import com.wdkl.webrtc.rtclib.EnumType;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.R;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+public abstract class SingleCallFragment extends Fragment {
+    private static final String TAG = "SingleCallFragment";
+    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() {
+        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);
+        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, 60 * 1000);//1分钟之后未接通,则挂断电话
+        }
+    }
+
+    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;
+        session.toggleSpeaker(true);
+        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);
+                }
+            }
+        }
+    }
+}

+ 13 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/Utils.java

@@ -0,0 +1,13 @@
+package com.wdkl.webrtc.demo.core.voip;
+
+import com.wdkl.webrtc.demo.App;
+
+/**
+ * Created by Allen.Fu on 2019/8/5.
+ * android_shuai@163.com
+ */
+public class Utils {
+
+    public static String ACTION_VOIP_RECEIVER = App.getInstance().getPackageName() + ".voip.Receiver";
+
+}

+ 112 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/VoipEvent.java

@@ -0,0 +1,112 @@
+package com.wdkl.webrtc.demo.core.voip;
+
+import android.media.AudioManager;
+import android.net.Uri;
+import android.util.Log;
+
+import com.wdkl.webrtc.demo.App;
+import com.wdkl.webrtc.demo.core.socket.SocketManager;
+import com.wdkl.webrtc.rtclib.inter.ISkyEvent;
+import com.wdkl.webrtc.rtclib.R;
+
+import java.util.List;
+
+/**
+ * Created by Allen.Fu 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://" + App.getInstance().getPackageName() + "/" + R.raw.incoming_call_ring);
+            ringPlayer.play(App.getInstance(), uri, true, AudioManager.STREAM_RING);
+        } else {
+            Uri uri = Uri.parse("android.resource://" + App.getInstance().getPackageName() + "/" + R.raw.wr_ringback);
+            ringPlayer.play(App.getInstance(), uri, true, AudioManager.STREAM_RING);
+        }
+    }
+
+    @Override
+    public void shouldStopRing() {
+        Log.d(TAG, "shouldStopRing begin");
+        ringPlayer.stop();
+    }
+}

+ 232 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/core/voip/VoipReceiver.java

@@ -0,0 +1,232 @@
+package com.wdkl.webrtc.demo.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.webrtc.demo.App;
+import com.wdkl.webrtc.demo.core.base.BaseActivity;
+import com.wdkl.webrtc.demo.core.util.ActivityStackManager;
+import com.wdkl.webrtc.demo.permission.Permissions;
+import com.wdkl.webrtc.rtclib.SkyEngineKit;
+import com.wdkl.webrtc.rtclib.R;
+import com.tapadoo.alerter.Alerter;
+
+import java.util.ArrayList;
+
+/**
+ * Created by Allen.Fu 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.webrtc.demo.core.util.Utils.isAppRunningForeground()) {
+                    onForegroundOrBeforeVersionO(room, userList, inviteId, audioOnly, inviteUserName, true);
+                } else {
+                    onBackgroundAfterVersionO(room, userList, inviteId, audioOnly, inviteUserName);
+                }
+            } else {
+                onForegroundOrBeforeVersionO(
+                        room,
+                        userList,
+                        inviteId,
+                        audioOnly,
+                        inviteUserName,
+                        com.wdkl.webrtc.demo.core.util.Utils.isAppRunningForeground()
+                );
+            }
+        }
+    }
+
+    private void onBackgroundAfterVersionO(
+            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 = Permissions.has(activity, per);
+        if (hasPermission) {
+            onBackgroundHasPermission(activity, room, list, inviteId, audioOnly, inviteUserName);
+        } else {
+            CallForegroundNotification notification = new CallForegroundNotification(App.getInstance());
+            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(App.getInstance(), room, inviteId, audioOnly);
+        LogUtils.dTag(TAG, "onBackgroundHasPermission b = " + b );
+        if (b) {
+            App.getInstance().setOtherUserId(inviteId);
+            if (list.size() == 1) {
+                CallForegroundNotification notification = new CallForegroundNotification(App.getInstance());
+                notification.sendIncomingCallNotification(
+                        App.getInstance(),
+                        inviteId,
+                        false,
+                        inviteUserName,
+                        audioOnly,
+                        true
+                );
+            }
+        }
+    }
+
+    private void onForegroundOrBeforeVersionO(
+            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 = Permissions.has(activity, per);
+        LogUtils.dTag(TAG, "onForegroundOrBeforeVersionO hasPermission = " + hasPermission + ", isForeGround = " + isForeGround);
+        if (hasPermission) {
+            onHasPermission(activity, 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(App.getInstance());
+                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(App.getInstance(), 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(App.getInstance(), "权限被拒绝,无法通话", Toast.LENGTH_SHORT).show();
+    }
+
+    private void shouldStartRing(boolean isComing) {
+        if (isComing) {
+            Uri uri = Uri.parse("android.resource://" + App.getInstance().getPackageName() + "/" + R.raw.incoming_call_ring);
+            ringPlayer.play(App.getInstance(), uri, true, AudioManager.STREAM_RING);
+        } else {
+            Uri uri = Uri.parse("android.resource://" + App.getInstance().getPackageName() + "/" + R.raw.wr_ringback);
+            ringPlayer.play(App.getInstance(), uri, true, AudioManager.STREAM_RING);
+        }
+    }
+
+    private void shouldStopRing() {
+        Log.d(TAG, "shouldStopRing begin");
+        ringPlayer.stop();
+    }
+}

+ 37 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/HttpRequest.java

@@ -0,0 +1,37 @@
+package com.wdkl.webrtc.demo.net;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by Allen.Fu 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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/HttpRequestPresenter.java

@@ -0,0 +1,47 @@
+package com.wdkl.webrtc.demo.net;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by Allen.Fu 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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/ICallback.java

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

+ 54 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/urlconn/UrlConnRequest.java

@@ -0,0 +1,54 @@
+package com.wdkl.webrtc.demo.net.urlconn;
+
+
+import com.wdkl.webrtc.demo.net.HttpRequest;
+import com.wdkl.webrtc.demo.net.ICallback;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by Allen.Fu 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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/net/urlconn/UrlConnUtils.java

@@ -0,0 +1,263 @@
+package com.wdkl.webrtc.demo.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 Allen.Fu 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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/permission/Consumer.java

@@ -0,0 +1,18 @@
+package com.wdkl.webrtc.demo.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
wdklRTC/src/main/java/com/wdkl/webrtc/demo/permission/Permissions.java

@@ -0,0 +1,144 @@
+package com.wdkl.webrtc.demo.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;
+    }
+}

+ 196 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/AVEngine.java

@@ -0,0 +1,196 @@
+package com.wdkl.webrtc.rtclib;
+
+import android.util.Log;
+import android.view.View;
+
+import com.wdkl.webrtc.rtclib.engine.EngineCallback;
+import com.wdkl.webrtc.rtclib.engine.IEngine;
+
+import java.util.List;
+
+public class AVEngine implements IEngine {
+    private static final String TAG = "AVEngine";
+    private final IEngine iEngine;
+    private static volatile AVEngine instance;
+
+    private AVEngine(IEngine engine) {
+        iEngine = engine;
+    }
+
+    public static AVEngine createEngine(IEngine engine) {
+        if (null == instance) {
+            synchronized (AVEngine.class) {
+                if (null == instance) {
+                    instance = new AVEngine(engine);
+                }
+            }
+        }
+
+        return instance;
+    }
+
+    @Override
+    public void init(EngineCallback callback) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.init(callback);
+    }
+
+    @Override
+    public void joinRoom(List<String> userIds) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.joinRoom(userIds);
+    }
+
+    @Override
+    public void userIn(String userId) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.userIn(userId);
+    }
+
+    @Override
+    public void userReject(String userId, int type) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.userReject(userId, type);
+    }
+
+    @Override
+    public void disconnected(String userId, EnumType.CallEndReason reason) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.disconnected(userId, reason);
+    }
+
+    @Override
+    public void receiveOffer(String userId, String description) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.receiveOffer(userId, description);
+    }
+
+    @Override
+    public void receiveAnswer(String userId, String sdp) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.receiveAnswer(userId, sdp);
+    }
+
+    @Override
+    public void receiveIceCandidate(String userId, String id, int label, String candidate) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.receiveIceCandidate(userId, id, label, candidate);
+    }
+
+
+    @Override
+    public void leaveRoom(String userId) {
+        Log.d(TAG, "leaveRoom iEngine = " + iEngine);
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.leaveRoom(userId);
+    }
+
+    @Override
+    public View startPreview(boolean isO) {
+        if (iEngine == null) {
+            return null;
+        }
+        return iEngine.startPreview(isO);
+    }
+
+    @Override
+    public void stopPreview() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.stopPreview();
+    }
+
+    @Override
+    public void startStream() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.startStream();
+    }
+
+    @Override
+    public void stopStream() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.stopStream();
+    }
+
+    @Override
+    public View setupRemoteVideo(String userId, boolean isO) {
+        if (iEngine == null) {
+            return null;
+        }
+        return iEngine.setupRemoteVideo(userId, isO);
+    }
+
+    @Override
+    public void stopRemoteVideo() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.stopRemoteVideo();
+    }
+
+
+    @Override
+    public void switchCamera() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.switchCamera();
+    }
+
+    @Override
+    public boolean muteAudio(boolean enable) {
+        if (iEngine == null) {
+            return false;
+        }
+        return iEngine.muteAudio(enable);
+    }
+
+    @Override
+    public boolean toggleSpeaker(boolean enable) {
+        if (iEngine == null) {
+            return false;
+        }
+        return iEngine.toggleSpeaker(enable);
+    }
+
+    @Override
+    public boolean toggleHeadset(boolean isHeadset) {
+        if (iEngine == null) {
+            return false;
+        }
+        return iEngine.toggleHeadset(isHeadset);
+    }
+
+    @Override
+    public void release() {
+        if (iEngine == null) {
+            return;
+        }
+         Log.d(TAG,"release");
+        iEngine.release();
+    }
+
+}

+ 12 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/BuildConfig.java

@@ -0,0 +1,12 @@
+/**
+ * Automatically generated file. DO NOT MODIFY
+ */
+package com.wdkl.webrtc.rtclib;
+
+public final class BuildConfig {
+  public static final boolean DEBUG = Boolean.parseBoolean("true");
+  public static final String APPLICATION_ID = "com.wdkl.webrtc.debug";
+  public static final String BUILD_TYPE = "debug";
+  public static final int VERSION_CODE = 100;
+  public static final String VERSION_NAME = "1.0.0";
+}

+ 549 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/CallSession.java

@@ -0,0 +1,549 @@
+package com.wdkl.webrtc.rtclib;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.wdkl.webrtc.rtclib.engine.EngineCallback;
+import com.wdkl.webrtc.rtclib.engine.webrtc.WebRTCEngine;
+import com.wdkl.webrtc.rtclib.inter.ISkyEvent;
+
+import org.webrtc.IceCandidate;
+import org.webrtc.SessionDescription;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 会话层
+ * Created by Allen.Fu on 2019/8/19.
+ *
+ */
+public class CallSession implements EngineCallback {
+    private static final String TAG = "CallSession";
+    private WeakReference<CallSessionCallback> sessionCallback;
+    private ExecutorService executor;
+    private Handler handler = new Handler(Looper.getMainLooper());
+    // session参数
+    private boolean mIsAudioOnly;
+    // 房间人列表
+    private List<String> mUserIDList;
+    // 单聊对方Id/群聊邀请人
+    public String mTargetId;
+    // 房间Id
+    private String mRoomId;
+    // myId
+    public String mMyId;
+    // 房间大小
+    private int mRoomSize;
+
+    private boolean mIsComing;
+    private EnumType.CallState _callState = EnumType.CallState.Idle;
+    private long startTime;
+
+    private AVEngine iEngine;
+    private ISkyEvent mEvent;
+
+    public CallSession(Context context, String roomId, boolean audioOnly, ISkyEvent event) {
+        executor = Executors.newSingleThreadExecutor();
+        this.mIsAudioOnly = audioOnly;
+        this.mRoomId = roomId;
+
+        this.mEvent = event;
+        iEngine = AVEngine.createEngine(new WebRTCEngine(audioOnly, context));
+        iEngine.init(this);
+    }
+
+
+    // ----------------------------------------各种控制--------------------------------------------
+
+    // 创建房间
+    public void createHome(String room, int roomSize) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                mEvent.createRoom(room, roomSize);
+            }
+        });
+    }
+
+    // 加入房间
+    public void joinHome(String roomId) {
+        executor.execute(() -> {
+            _callState = EnumType.CallState.Connecting;
+             Log.d(TAG, "joinHome mEvent = " + mEvent);
+            setIsComing(true);
+            if (mEvent != null) {
+                mEvent.sendJoin(roomId);
+            }
+        });
+
+    }
+
+    //开始响铃
+    public void shouldStartRing() {
+        if (mEvent != null) {
+            mEvent.shouldStartRing(true);
+        }
+    }
+
+    // 关闭响铃
+    public void shouldStopRing() {
+        Log.d(TAG, "shouldStopRing mEvent != null is " + (mEvent != null));
+        if (mEvent != null) {
+            mEvent.shouldStopRing();
+        }
+    }
+
+    // 发送响铃回复
+    public void sendRingBack(String targetId, String room) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                mEvent.sendRingBack(targetId, room);
+            }
+        });
+    }
+
+    // 发送拒绝信令
+    public void sendRefuse() {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                // 取消拨出
+                mEvent.sendRefuse(mRoomId, mTargetId, EnumType.RefuseType.Hangup.ordinal());
+            }
+        });
+		release(EnumType.CallEndReason.Hangup);
+    }
+
+    // 发送忙时拒绝
+    void sendBusyRefuse(String room, String targetId) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                // 取消拨出
+                mEvent.sendRefuse(room, targetId, EnumType.RefuseType.Busy.ordinal());
+            }
+        });
+		release(EnumType.CallEndReason.Hangup);
+
+    }
+
+    // 发送取消信令
+    public void sendCancel() {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                // 取消拨出
+                List<String> list = new ArrayList<>();
+                list.add(mTargetId);
+                mEvent.sendCancel(mRoomId, list);
+            }
+        });
+		release(EnumType.CallEndReason.Hangup);
+
+    }
+
+    // 离开房间
+    public void leave() {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                mEvent.sendLeave(mRoomId, mMyId);
+            }
+        });
+        // 释放变量
+        release(EnumType.CallEndReason.Hangup);
+
+    }
+
+    // 切换到语音接听
+    public void sendTransAudio() {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                // 发送到对面,切换到语音
+                mEvent.sendTransAudio(mTargetId);
+            }
+        });
+    }
+
+    // 设置静音
+    public boolean toggleMuteAudio(boolean enable) {
+        return iEngine.muteAudio(enable);
+    }
+
+    // 设置扬声器
+    public boolean toggleSpeaker(boolean enable) {
+
+        return iEngine.toggleSpeaker(enable);
+    }
+
+    // 设置扬声器
+    public boolean toggleHeadset(boolean isHeadset) {
+        return iEngine.toggleHeadset(isHeadset);
+    }
+
+    // 切换到语音通话
+    public void switchToAudio() {
+        mIsAudioOnly = true;
+        // 告诉远端
+        sendTransAudio();
+        // 本地切换
+        if (sessionCallback != null && sessionCallback.get() != null) {
+            sessionCallback.get().didChangeMode(true);
+        }
+
+    }
+
+    // 调整摄像头前置后置
+    public void switchCamera() {
+        iEngine.switchCamera();
+    }
+
+    // 释放资源
+    private void release(EnumType.CallEndReason reason) {
+        executor.execute(() -> {
+            // 释放内容
+            iEngine.release();
+            // 状态设置为Idle
+            _callState = EnumType.CallState.Idle;
+
+            //界面回调
+            if (sessionCallback != null && sessionCallback.get() != null) {
+                sessionCallback.get().didCallEndWithReason(reason);
+            } else {
+				//TODO 结束会话
+			}
+        });
+    }
+
+    //------------------------------------receive---------------------------------------------------
+
+    // 加入房间成功
+    public void onJoinHome(String myId, String users, int roomSize) {
+        // 开始计时
+        mRoomSize = roomSize;
+        startTime = 0;
+        handler.post(() -> executor.execute(() -> {
+            mMyId = myId;
+            List<String> strings;
+            if (!TextUtils.isEmpty(users)) {
+                String[] split = users.split(",");
+                strings = Arrays.asList(split);
+                mUserIDList = strings;
+            }
+
+            // 发送邀请
+            if (!mIsComing) {
+                if (roomSize == 2) {
+                    List<String> inviteList = new ArrayList<>();
+                    inviteList.add(mTargetId);
+                    mEvent.sendInvite(mRoomId, inviteList, mIsAudioOnly);
+                }
+            } else {
+                iEngine.joinRoom(mUserIDList);
+            }
+
+            if (!isAudioOnly()) {
+                // 画面预览
+                if (sessionCallback != null && sessionCallback.get() != null) {
+                    sessionCallback.get().didCreateLocalVideoTrack();
+                }
+
+            }
+
+
+        }));
+
+
+    }
+
+    // 新成员进入
+    public void newPeer(String userId) {
+        handler.post(() -> executor.execute(() -> {
+            // 其他人加入房间
+            iEngine.userIn(userId);
+
+            // 关闭响铃
+            if (mEvent != null) {
+                mEvent.shouldStopRing();
+            }
+            // 更换界面
+            _callState = EnumType.CallState.Connected;
+
+            if (sessionCallback != null && sessionCallback.get() != null) {
+                startTime = System.currentTimeMillis();
+                sessionCallback.get().didChangeState(_callState);
+
+            }
+        }));
+
+    }
+
+    // 对方已拒绝
+    public void onRefuse(String userId, int type) {
+        iEngine.userReject(userId, type);
+    }
+
+    // 对方已响铃
+    public void onRingBack(String userId) {
+        if (mEvent != null) {
+            mEvent.onRemoteRing();
+            //mEvent.shouldStartRing(false);
+        }
+    }
+
+    // 切换到语音
+    public void onTransAudio(String userId) {
+        mIsAudioOnly = true;
+        // 本地切换
+        if (sessionCallback != null && sessionCallback.get() != null) {
+            sessionCallback.get().didChangeMode(true);
+        }
+    }
+
+    // 对方网络断开
+    public void onDisConnect(String userId, EnumType.CallEndReason reason) {
+        executor.execute(() -> {
+            iEngine.disconnected(userId, reason);
+        });
+    }
+
+    // 对方取消拨出
+    public void onCancel(String userId) {
+        Log.d(TAG, "onCancel userId = " + userId);
+        shouldStopRing();
+        release(EnumType.CallEndReason.RemoteHangup);
+    }
+
+    public void onReceiveOffer(String userId, String description) {
+        executor.execute(() -> {
+            iEngine.receiveOffer(userId, description);
+        });
+
+    }
+
+    public void onReceiverAnswer(String userId, String sdp) {
+        executor.execute(() -> {
+            iEngine.receiveAnswer(userId, sdp);
+        });
+
+    }
+
+    public void onRemoteIceCandidate(String userId, String id, int label, String candidate) {
+        executor.execute(() -> {
+            iEngine.receiveIceCandidate(userId, id, label, candidate);
+        });
+
+    }
+
+    // 对方离开房间
+    public void onLeave(String userId) {
+        if (mRoomSize > 2) {
+            // 返回到界面上
+            if (sessionCallback != null && sessionCallback.get() != null) {
+                sessionCallback.get().didUserLeave(userId);
+            }
+        }
+        executor.execute(() -> iEngine.leaveRoom(userId));
+
+
+    }
+
+
+    // --------------------------------界面显示相关--------------------------------------------/
+
+    public long getStartTime() {
+        return startTime;
+    }
+
+    public View setupLocalVideo(boolean isOverlay) {
+        return iEngine.startPreview(isOverlay);
+    }
+
+
+    public View setupRemoteVideo(String userId, boolean isOverlay) {
+        return iEngine.setupRemoteVideo(userId, isOverlay);
+    }
+
+
+    //------------------------------------各种参数----------------------------------------------/
+
+    public void setIsAudioOnly(boolean _isAudioOnly) {
+        this.mIsAudioOnly = _isAudioOnly;
+    }
+
+    public boolean isAudioOnly() {
+        return mIsAudioOnly;
+    }
+
+    public void setTargetId(String targetIds) {
+        this.mTargetId = targetIds;
+    }
+
+    public void setIsComing(boolean isComing) {
+        this.mIsComing = isComing;
+    }
+
+    public boolean isComing() {
+        return mIsComing;
+    }
+
+    public void setRoom(String _room) {
+        this.mRoomId = _room;
+    }
+
+    public String getRoomId() {
+        return mRoomId;
+    }
+
+    public EnumType.CallState getState() {
+        return _callState;
+    }
+
+    public void setCallState(EnumType.CallState callState) {
+        this._callState = callState;
+    }
+
+    public void setSessionCallback(CallSessionCallback sessionCallback) {
+        this.sessionCallback = new WeakReference<>(sessionCallback);
+    }
+
+    //-----------------------------Engine回调-----------------------------------------
+
+    @Override
+    public void joinRoomSucc() {
+        // 关闭响铃
+        if (mEvent != null) {
+            mEvent.shouldStopRing();
+        }
+        // 更换界面
+        _callState = EnumType.CallState.Connected;
+        //Log.d(TAG, "joinRoomSucc, sessionCallback.get() = " + sessionCallback.get());
+        if (sessionCallback != null && sessionCallback.get() != null) {
+            startTime = System.currentTimeMillis();
+            sessionCallback.get().didChangeState(_callState);
+
+        }
+    }
+
+    @Override
+    public void exitRoom() {
+        // 状态设置为Idle
+        if (mRoomSize == 2) {
+            handler.post(() -> {
+                release(EnumType.CallEndReason.RemoteHangup);
+            });
+        }
+
+
+    }
+
+    @Override
+    public void reject(int type) {
+        shouldStopRing();
+        Log.d(TAG, "reject type = " + type);
+//        handler.post(() -> {
+        switch (type) {
+            case 0:
+                release(EnumType.CallEndReason.Busy);
+                break;
+            case 1:
+                release(EnumType.CallEndReason.RemoteHangup);
+                break;
+
+        }
+//        });
+    }
+
+    @Override
+    public void disconnected(EnumType.CallEndReason reason) {
+        handler.post(() -> {
+            shouldStopRing();
+            release(reason);
+        });
+    }
+
+    @Override
+    public void onSendIceCandidate(String userId, IceCandidate candidate) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                Log.d("dds_test", "onSendIceCandidate");
+                mEvent.sendIceCandidate(userId, candidate.sdpMid, candidate.sdpMLineIndex, candidate.sdp);
+            }
+        });
+
+    }
+
+    @Override
+    public void onSendOffer(String userId, SessionDescription description) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                Log.d("dds_test", "onSendOffer");
+                mEvent.sendOffer(userId, description.description);
+            }
+        });
+
+    }
+
+    @Override
+    public void onSendAnswer(String userId, SessionDescription description) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                Log.d("dds_test", "onSendAnswer");
+                mEvent.sendAnswer(userId, description.description);
+            }
+        });
+
+    }
+
+    @Override
+    public void onRemoteStream(String userId) {
+        // 画面预览
+        if (sessionCallback != null && sessionCallback.get() != null) {
+           Log.d(TAG, "onRemoteStream sessionCallback.get() != null ");
+            sessionCallback.get().didReceiveRemoteVideoTrack(userId);
+        } else {
+            Log.d(TAG, "onRemoteStream sessionCallback.get() == null ");
+        }
+    }
+
+    @Override
+    public void onDisconnected(String userId) {
+        //断线了,需要关闭通话界面
+        if (sessionCallback != null && sessionCallback.get() != null) {
+           Log.d(TAG, "onDisconnected sessionCallback.get() != null ");
+            sessionCallback.get().didDisconnected(userId);
+        } else {
+            Log.d(TAG, "onDisconnected sessionCallback.get() == null ");
+        }
+    }
+
+    public interface CallSessionCallback {
+        void didCallEndWithReason(EnumType.CallEndReason var1);
+
+        void didChangeState(EnumType.CallState var1);
+
+        void didChangeMode(boolean isAudioOnly);
+
+        void didCreateLocalVideoTrack();
+
+        void didReceiveRemoteVideoTrack(String userId);
+
+        void didUserLeave(String userId);
+
+        void didError(String error);
+
+        void didDisconnected(String userId);
+
+    }
+
+
+}

+ 41 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/EnumType.java

@@ -0,0 +1,41 @@
+package com.wdkl.webrtc.rtclib;
+
+/**
+ * Created by Allen.Fu on 2019/8/22.
+ * android_shuai@163.com
+ */
+public class EnumType {
+
+    public enum CallState {
+        Idle,
+        Outgoing,
+        Incoming,
+        Connecting,
+        Connected;
+
+        CallState() {
+        }
+    }
+
+    public enum CallEndReason {
+        Busy,
+        SignalError,
+        RemoteSignalError,
+        Hangup,
+        MediaError,
+        RemoteHangup,
+        OpenCameraFailure,
+        Timeout,
+        AcceptByOtherClient;
+
+        CallEndReason() {
+        }
+    }
+
+    public enum RefuseType {
+        Busy,
+        Hangup,
+    }
+
+
+}

+ 206 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/SkyEngineKit.java

@@ -0,0 +1,206 @@
+package com.wdkl.webrtc.rtclib;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.wdkl.webrtc.rtclib.except.NotInitializedException;
+import com.wdkl.webrtc.rtclib.inter.ISkyEvent;
+
+/**
+ * 主控类
+ * Created by Allen.Fu.
+ */
+public class SkyEngineKit {
+    private final static String TAG = "dds_AVEngineKit";
+    private static SkyEngineKit avEngineKit;
+    private CallSession mCurrentCallSession;
+    private ISkyEvent mEvent;
+    private boolean isAudioOnly = false;
+    private boolean isOutGoing = false;
+
+
+    public static SkyEngineKit Instance() {
+        SkyEngineKit var;
+        if ((var = avEngineKit) != null) {
+            return var;
+        } else {
+            throw new NotInitializedException();
+        }
+    }
+
+    // 初始化
+    public static void init(ISkyEvent iSocketEvent) {
+        if (avEngineKit == null) {
+            avEngineKit = new SkyEngineKit();
+            avEngineKit.mEvent = iSocketEvent;
+        }
+    }
+
+
+    public void sendRefuseOnPermissionDenied(String room, String inviteId) {
+        // 未初始化
+        if (avEngineKit == null) {
+            Log.e(TAG, "startOutCall error,please init first");
+            return;
+        }
+        if (mCurrentCallSession != null) {
+            endCall();
+        } else {
+            avEngineKit.mEvent.sendRefuse(room, inviteId, EnumType.RefuseType.Hangup.ordinal());
+        }
+    }
+
+    public void sendDisconnected(String room, String toId, boolean isCrashed) {
+        // 未初始化
+        if (avEngineKit == null) {
+            Log.e(TAG, "startOutCall error,please init first");
+            return;
+        }
+        avEngineKit.mEvent.sendDisConnect(room, toId, isCrashed);
+    }
+
+    // 拨打电话
+    public boolean startOutCall(Context context, final String room, final String targetId,
+                                final boolean audioOnly) {
+        // 未初始化
+        if (avEngineKit == null) {
+            Log.e(TAG, "startOutCall error,please init first");
+            return false;
+        }
+        // 忙线中
+        if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
+            Log.i(TAG, "startCall error,currentCallSession is exist");
+            return false;
+        }
+        isAudioOnly = audioOnly;
+        isOutGoing = true;
+        // 初始化会话
+        mCurrentCallSession = new CallSession(context, room, audioOnly, mEvent);
+        mCurrentCallSession.setTargetId(targetId);
+        mCurrentCallSession.setIsComing(false);
+        mCurrentCallSession.setCallState(EnumType.CallState.Outgoing);
+        // 创建房间
+        mCurrentCallSession.createHome(room, 2);
+
+
+        return true;
+    }
+
+    // 接听电话
+    public boolean startInCall(Context context, final String room, final String targetId,
+                               final boolean audioOnly) {
+        if (avEngineKit == null) {
+            Log.e(TAG, "startInCall error,init is not set");
+            return false;
+        }
+        // 忙线中
+        if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
+            // 发送->忙线中...
+            Log.i(TAG, "startInCall busy,currentCallSession is exist,start sendBusyRefuse!");
+            mCurrentCallSession.sendBusyRefuse(room, targetId);
+            return false;
+        }
+		isOutGoing = false;
+		this.isAudioOnly = audioOnly;
+        // 初始化会话
+        mCurrentCallSession = new CallSession(context, room, audioOnly, mEvent);
+        mCurrentCallSession.setTargetId(targetId);
+        mCurrentCallSession.setIsComing(true);
+        mCurrentCallSession.setCallState(EnumType.CallState.Incoming);
+
+        // 开始响铃并回复
+        mCurrentCallSession.shouldStartRing();
+        mCurrentCallSession.sendRingBack(targetId, room);
+
+
+        return true;
+    }
+
+    // 挂断会话
+    public void endCall() {
+        Log.d(TAG, "endCall mCurrentCallSession != null is " + (mCurrentCallSession != null));
+        if (mCurrentCallSession != null) {
+            // 停止响铃
+            mCurrentCallSession.shouldStopRing();
+
+            if (mCurrentCallSession.isComing()) {
+                if (mCurrentCallSession.getState() == EnumType.CallState.Incoming) {
+                    // 接收到邀请,还没同意,发送拒绝
+                    mCurrentCallSession.sendRefuse();
+                } else {
+                    // 已经接通,挂断电话
+                    mCurrentCallSession.leave();
+                }
+            } else {
+                if (mCurrentCallSession.getState() == EnumType.CallState.Outgoing) {
+                    mCurrentCallSession.sendCancel();
+                } else {
+                    // 已经接通,挂断电话
+                    mCurrentCallSession.leave();
+                }
+            }
+            mCurrentCallSession.setCallState(EnumType.CallState.Idle);
+        }
+
+    }
+
+    // 加入房间
+    public void joinRoom(Context context, String room) {
+        if (avEngineKit == null) {
+            Log.e(TAG, "joinRoom error,init is not set");
+            return;
+        }
+        // 忙线中
+        if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
+            Log.e(TAG, "joinRoom error,currentCallSession is exist");
+            return;
+        }
+        mCurrentCallSession = new CallSession(context, room, false, mEvent);
+        mCurrentCallSession.setIsComing(true);
+        mCurrentCallSession.joinHome(room);
+    }
+
+    public void createAndJoinRoom(Context context, String room) {
+        if (avEngineKit == null) {
+            Log.e(TAG, "joinRoom error,init is not set");
+            return;
+        }
+        // 忙线中
+        if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
+            Log.e(TAG, "joinRoom error,currentCallSession is exist");
+            return;
+        }
+        mCurrentCallSession = new CallSession(context, room, false, mEvent);
+        mCurrentCallSession.setIsComing(false);
+        mCurrentCallSession.createHome(room, 9);
+    }
+
+    // 离开房间
+    public void leaveRoom() {
+        if (avEngineKit == null) {
+            Log.e(TAG, "leaveRoom error,init is not set");
+            return;
+        }
+        if (mCurrentCallSession != null) {
+            mCurrentCallSession.leave();
+            mCurrentCallSession.setCallState(EnumType.CallState.Idle);
+        }
+    }
+
+    public void transferToAudio() {
+        isAudioOnly = true;
+    }
+    public boolean isOutGoing() {
+        return isOutGoing;
+    }
+
+    public boolean isAudioOnly() {
+        return isAudioOnly;
+    }
+    // 获取对话实例
+    public CallSession getCurrentSession() {
+        return this.mCurrentCallSession;
+    }
+
+
+}

+ 43 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/engine/EngineCallback.java

@@ -0,0 +1,43 @@
+package com.wdkl.webrtc.rtclib.engine;
+
+import com.wdkl.webrtc.rtclib.EnumType;
+
+import org.webrtc.IceCandidate;
+import org.webrtc.SessionDescription;
+
+/**
+ * Created by Allen.Fu on 2020/4/12.
+ * 框架回调
+ */
+public interface EngineCallback {
+
+
+    /**
+     * 加入房间成功
+     */
+    void joinRoomSucc();
+
+    /**
+     * 退出房间成功
+     */
+    void exitRoom();
+
+    /**
+     * 拒绝连接
+     * @param type type
+     */
+    void reject(int type);
+
+    void disconnected(EnumType.CallEndReason reason);
+
+    void onSendIceCandidate(String userId, IceCandidate candidate);
+
+    void onSendOffer(String userId, SessionDescription description);
+
+    void onSendAnswer(String userId, SessionDescription description);
+
+    void onRemoteStream(String userId);
+
+    void onDisconnected(String userId);
+
+}

+ 121 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/engine/IEngine.java

@@ -0,0 +1,121 @@
+package com.wdkl.webrtc.rtclib.engine;
+
+
+import android.view.View;
+
+import com.wdkl.webrtc.rtclib.EnumType;
+
+import java.util.List;
+
+/**
+ * rtc基类
+ */
+public interface IEngine {
+
+    /**
+     * 初始化
+     */
+    void init(EngineCallback callback);
+
+    /**
+     * 加入房間
+     */
+    void joinRoom(List<String> userIds);
+
+    /**
+     * 有人进入房间
+     */
+    void userIn(String userId);
+
+    /**
+     * 用户拒绝
+     * @param userId userId
+     * @param type type
+     */
+    void userReject(String userId,int type);
+
+    /**
+     * 用户网络断开
+     * @param userId userId
+     * @param reason
+     */
+    void disconnected(String userId, EnumType.CallEndReason reason);
+
+    /**
+     * receive Offer
+     */
+    void receiveOffer(String userId, String description);
+
+    /**
+     * receive Answer
+     */
+    void receiveAnswer(String userId, String sdp);
+
+    /**
+     * receive IceCandidate
+     */
+    void receiveIceCandidate(String userId, String id, int label, String candidate);
+
+    /**
+     * 离开房间
+     *
+     * @param userId userId
+     */
+    void leaveRoom(String userId);
+
+    /**
+     * 开启本地预览
+     */
+    View startPreview(boolean isOverlay);
+
+    /**
+     * 关闭本地预览
+     */
+    void stopPreview();
+
+    /**
+     * 开始远端推流
+     */
+    void startStream();
+
+    /**
+     * 停止远端推流
+     */
+    void stopStream();
+
+    /**
+     * 开始远端预览
+     */
+    View setupRemoteVideo(String userId, boolean isO);
+
+    /**
+     * 关闭远端预览
+     */
+    void stopRemoteVideo();
+
+    /**
+     * 切换摄像头
+     */
+    void switchCamera();
+
+    /**
+     * 设置静音
+     */
+    boolean muteAudio(boolean enable);
+
+    /**
+     * 开启扬声器
+     */
+    boolean toggleSpeaker(boolean enable);
+
+    /**
+     * 切换外放和耳机
+     */
+    boolean toggleHeadset(boolean isHeadset);
+
+    /**
+     * 释放所有内容
+     */
+    void release();
+
+}

+ 354 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/engine/webrtc/Peer.java

@@ -0,0 +1,354 @@
+package com.wdkl.webrtc.rtclib.engine.webrtc;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.wdkl.webrtc.rtclib.render.ProxyVideoSink;
+
+import org.webrtc.DataChannel;
+import org.webrtc.EglBase;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.RendererCommon;
+import org.webrtc.RtpReceiver;
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+import org.webrtc.SurfaceViewRenderer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Allen.Fu on 2020/3/11.
+ */
+public class Peer implements SdpObserver, PeerConnection.Observer {
+    private final static String TAG = "dds_Peer";
+    private final PeerConnection pc;
+    private final String mUserId;
+    private List<IceCandidate> queuedRemoteCandidates;
+    private SessionDescription localSdp;
+    private final PeerConnectionFactory mFactory;
+    private final List<PeerConnection.IceServer> mIceLis;
+    private final IPeerEvent mEvent;
+    private boolean isOffer;
+
+    public MediaStream _remoteStream;
+    public SurfaceViewRenderer renderer;
+    public ProxyVideoSink sink;
+
+
+    public Peer(PeerConnectionFactory factory, List<PeerConnection.IceServer> list, String userId, IPeerEvent event) {
+        mFactory = factory;
+        mIceLis = list;
+        mEvent = event;
+        mUserId = userId;
+        queuedRemoteCandidates = new ArrayList<>();
+        this.pc = createPeerConnection();
+        Log.d("dds_test", "create Peer:" + mUserId);
+
+    }
+
+    public PeerConnection createPeerConnection() {
+        PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(mIceLis);
+        if (mFactory != null) {
+            return mFactory.createPeerConnection(rtcConfig, this);
+        } else {
+            return null;
+        }
+    }
+
+    public void setOffer(boolean isOffer) {
+        this.isOffer = isOffer;
+    }
+
+    // 创建offer
+    public void createOffer() {
+        if (pc == null) return;
+        Log.d("dds_test", "createOffer");
+        pc.createOffer(this, offerOrAnswerConstraint());
+    }
+
+    // 创建answer
+    public void createAnswer() {
+        if (pc == null) return;
+        Log.d("dds_test", "createAnswer");
+        pc.createAnswer(this, offerOrAnswerConstraint());
+
+    }
+
+    // 设置LocalDescription
+    public void setLocalDescription(SessionDescription sdp) {
+        Log.d("dds_test", "setLocalDescription");
+        if (pc == null) return;
+        pc.setLocalDescription(this, sdp);
+    }
+
+    // 设置RemoteDescription
+    public void setRemoteDescription(SessionDescription sdp) {
+        if (pc == null) return;
+        Log.d("dds_test", "setRemoteDescription");
+        pc.setRemoteDescription(this, sdp);
+    }
+
+    //添加本地流
+    public void addLocalStream(MediaStream stream) {
+        if (pc == null) return;
+        Log.d("dds_test", "addLocalStream" + mUserId);
+        pc.addStream(stream);
+    }
+
+    // 添加RemoteIceCandidate
+    public synchronized void addRemoteIceCandidate(final IceCandidate candidate) {
+        Log.d("dds_test", "addRemoteIceCandidate");
+        if (pc != null) {
+            if (queuedRemoteCandidates != null) {
+               Log.d("dds_test", "addRemoteIceCandidate  2222");
+                synchronized (Peer.class) {
+                    if (queuedRemoteCandidates != null) {
+                        queuedRemoteCandidates.add(candidate);
+                    }
+                }
+
+            } else {
+               Log.d("dds_test", "addRemoteIceCandidate1111");
+                pc.addIceCandidate(candidate);
+            }
+        }
+    }
+
+    // 移除RemoteIceCandidates
+    public void removeRemoteIceCandidates(final IceCandidate[] candidates) {
+        if (pc == null) {
+            return;
+        }
+        drainCandidates();
+        pc.removeIceCandidates(candidates);
+    }
+
+    public void createRender(EglBase mRootEglBase, Context context, boolean isOverlay) {
+        renderer = new SurfaceViewRenderer(context);
+        renderer.init(mRootEglBase.getEglBaseContext(), new RendererCommon.RendererEvents() {
+            @Override
+            public void onFirstFrameRendered() {
+                Log.d(TAG, "createRender onFirstFrameRendered");
+
+            }
+
+            @Override
+            public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) {
+                Log.d(TAG, "createRender onFrameResolutionChanged");
+            }
+        });
+        renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
+        renderer.setMirror(true);
+        renderer.setZOrderMediaOverlay(isOverlay);
+        sink = new ProxyVideoSink();
+        sink.setTarget(renderer);
+        if (_remoteStream != null && _remoteStream.videoTracks.size() > 0) {
+            _remoteStream.videoTracks.get(0).addSink(sink);
+        }
+
+    }
+
+    // 关闭Peer
+    public void close() {
+        if (renderer != null) {
+            renderer.release();
+            renderer = null;
+        }
+        if (sink != null) {
+            sink.setTarget(null);
+        }
+        if (pc != null) {
+            try {
+                pc.close();
+                pc.dispose();
+            } catch (Exception e) {
+
+            }
+
+
+        }
+
+
+    }
+
+    //------------------------------Observer-------------------------------------
+    @Override
+    public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+        Log.i(TAG, "onSignalingChange: " + signalingState);
+    }
+
+    @Override
+    public void onIceConnectionChange(PeerConnection.IceConnectionState newState) {
+        Log.i(TAG, "onIceConnectionChange: " + newState);
+        if (newState == PeerConnection.IceConnectionState.DISCONNECTED || newState == PeerConnection.IceConnectionState.FAILED) {
+            mEvent.onDisconnected(mUserId);
+        }
+
+    }
+
+    @Override
+    public void onIceConnectionReceivingChange(boolean receiving) {
+        Log.i(TAG, "onIceConnectionReceivingChange:" + receiving);
+    }
+
+    @Override
+    public void onIceGatheringChange(PeerConnection.IceGatheringState newState) {
+        Log.i(TAG, "onIceGatheringChange:" + newState.toString());
+    }
+
+    @Override
+    public void onIceCandidate(IceCandidate candidate) {
+        // 发送IceCandidate
+        mEvent.onSendIceCandidate(mUserId, candidate);
+    }
+
+    @Override
+    public void onIceCandidatesRemoved(IceCandidate[] candidates) {
+        Log.i(TAG, "onIceCandidatesRemoved:");
+    }
+
+    @Override
+    public void onAddStream(MediaStream stream) {
+        Log.i(TAG, "onAddStream:");
+        stream.audioTracks.get(0).setEnabled(true);
+        _remoteStream = stream;
+        if (mEvent != null) {
+            mEvent.onRemoteStream(mUserId, stream);
+        }
+    }
+
+    @Override
+    public void onRemoveStream(MediaStream stream) {
+        Log.i(TAG, "onRemoveStream:");
+        if (mEvent != null) {
+            mEvent.onRemoveStream(mUserId, stream);
+        }
+    }
+
+    @Override
+    public void onDataChannel(DataChannel dataChannel) {
+        Log.i(TAG, "onDataChannel:");
+    }
+
+    @Override
+    public void onRenegotiationNeeded() {
+        Log.i(TAG, "onRenegotiationNeeded:");
+    }
+
+    @Override
+    public void onAddTrack(RtpReceiver receiver, MediaStream[] mediaStreams) {
+        Log.i(TAG, "onAddTrack:" + mediaStreams.length);
+    }
+
+
+    //-------------SdpObserver--------------------
+    @Override
+    public void onCreateSuccess(SessionDescription origSdp) {
+        Log.d(TAG, "sdp创建成功       " + origSdp.type);
+        String sdpString = origSdp.description;
+        final SessionDescription sdp = new SessionDescription(origSdp.type, sdpString);
+        localSdp = sdp;
+        setLocalDescription(sdp);
+
+    }
+
+    @Override
+    public void onSetSuccess() {
+        Log.d(TAG, "sdp连接成功   " + pc.signalingState().toString());
+        if (pc == null) return;
+        // 发送者
+        if (isOffer) {
+            if (pc.getRemoteDescription() == null) {
+                Log.d(TAG, "Local SDP set succesfully");
+                if (!isOffer) {
+                    //接收者,发送Answer
+                    mEvent.onSendAnswer(mUserId, localSdp);
+                } else {
+                    //发送者,发送自己的offer
+                    mEvent.onSendOffer(mUserId, localSdp);
+                }
+            } else {
+                Log.d(TAG, "Remote SDP set succesfully");
+
+                drainCandidates();
+            }
+
+        } else {
+            if (pc.getLocalDescription() != null) {
+                Log.d(TAG, "Local SDP set succesfully");
+                if (!isOffer) {
+                    //接收者,发送Answer
+                    mEvent.onSendAnswer(mUserId, localSdp);
+                } else {
+                    //发送者,发送自己的offer
+                    mEvent.onSendOffer(mUserId, localSdp);
+                }
+
+                drainCandidates();
+            } else {
+                Log.d(TAG, "Remote SDP set succesfully");
+            }
+        }
+
+
+    }
+
+    @Override
+    public void onCreateFailure(String error) {
+        Log.i(TAG, " SdpObserver onCreateFailure:" + error);
+    }
+
+    @Override
+    public void onSetFailure(String error) {
+        Log.i(TAG, "SdpObserver onSetFailure:" + error);
+    }
+
+
+    private void drainCandidates() {
+        Log.i("dds_test", "drainCandidates");
+        synchronized (Peer.class) {
+            if (queuedRemoteCandidates != null) {
+                Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
+                for (IceCandidate candidate : queuedRemoteCandidates) {
+                    pc.addIceCandidate(candidate);
+                }
+                queuedRemoteCandidates = null;
+            }
+
+        }
+    }
+
+    private MediaConstraints offerOrAnswerConstraint() {
+        MediaConstraints mediaConstraints = new MediaConstraints();
+        ArrayList<MediaConstraints.KeyValuePair> keyValuePairs = new ArrayList<>();
+        keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
+        keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
+        mediaConstraints.mandatory.addAll(keyValuePairs);
+        return mediaConstraints;
+    }
+
+    // ----------------------------回调-----------------------------------
+
+    public interface IPeerEvent {
+
+
+        void onSendIceCandidate(String userId, IceCandidate candidate);
+
+        void onSendOffer(String userId, SessionDescription description);
+
+        void onSendAnswer(String userId, SessionDescription description);
+
+        void onRemoteStream(String userId, MediaStream stream);
+
+        void onRemoveStream(String userId, MediaStream stream);
+
+
+        void onDisconnected(String userId);
+    }
+
+}

+ 696 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/engine/webrtc/WebRTCEngine.java

@@ -0,0 +1,696 @@
+package com.wdkl.webrtc.rtclib.engine.webrtc;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.projection.MediaProjection;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.wdkl.webrtc.rtclib.EnumType;
+import com.wdkl.webrtc.rtclib.engine.EngineCallback;
+import com.wdkl.webrtc.rtclib.engine.IEngine;
+import com.wdkl.webrtc.rtclib.render.ProxyVideoSink;
+
+import org.webrtc.AudioSource;
+import org.webrtc.AudioTrack;
+import org.webrtc.Camera1Enumerator;
+import org.webrtc.Camera2Enumerator;
+import org.webrtc.CameraEnumerator;
+import org.webrtc.CameraVideoCapturer;
+import org.webrtc.DefaultVideoDecoderFactory;
+import org.webrtc.DefaultVideoEncoderFactory;
+import org.webrtc.EglBase;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.RendererCommon;
+import org.webrtc.ScreenCapturerAndroid;
+import org.webrtc.SessionDescription;
+import org.webrtc.SurfaceTextureHelper;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoCapturer;
+import org.webrtc.VideoDecoderFactory;
+import org.webrtc.VideoEncoderFactory;
+import org.webrtc.VideoSource;
+import org.webrtc.VideoTrack;
+import org.webrtc.audio.AudioDeviceModule;
+import org.webrtc.audio.JavaAudioDeviceModule;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class WebRTCEngine implements IEngine, Peer.IPeerEvent {
+    private static final String TAG = "WebRTCEngine";
+    private PeerConnectionFactory _factory;
+    private EglBase mRootEglBase;
+    private MediaStream _localStream;
+    private VideoSource videoSource;
+    private AudioSource audioSource;
+    private AudioTrack _localAudioTrack;
+    private VideoCapturer captureAndroid;
+    private SurfaceTextureHelper surfaceTextureHelper;
+
+    private ProxyVideoSink localSink;
+    private SurfaceViewRenderer localRenderer;
+
+
+    private static final String VIDEO_TRACK_ID = "ARDAMSv0";
+    private static final String AUDIO_TRACK_ID = "ARDAMSa0";
+    public static final String VIDEO_CODEC_H264 = "H264";
+    private static final int VIDEO_RESOLUTION_WIDTH = 640;
+    private static final int VIDEO_RESOLUTION_HEIGHT = 480;
+    private static final int FPS = 20;
+
+    // 对话实例列表
+    private ConcurrentHashMap<String, Peer> peers = new ConcurrentHashMap<>();
+    // 服务器实例列表
+    private String serverIP = "8.130.170.205";
+    private String turnUser = "dsg";
+    private String turnUserPwd = "123456";
+    private List<PeerConnection.IceServer> iceServers = new ArrayList<>();
+
+    private EngineCallback mCallback;
+
+    public boolean mIsAudioOnly;
+    private Context mContext;
+    private AudioManager audioManager;
+    private boolean isSpeakerOn = true;
+
+    public WebRTCEngine(boolean mIsAudioOnly, Context mContext) {
+        this.mIsAudioOnly = mIsAudioOnly;
+        this.mContext = mContext;
+        audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        // 初始化ice地址
+        initIceServer();
+    }
+
+
+    // -----------------------------------对外方法------------------------------------------
+    @Override
+    public void init(EngineCallback callback) {
+        mCallback = callback;
+
+        if (mRootEglBase == null) {
+            mRootEglBase = EglBase.create();
+        }
+        if (_factory == null) {
+            _factory = createConnectionFactory();
+        }
+        if (_localStream == null) {
+            createLocalStream();
+        }
+    }
+
+    @Override
+    public void joinRoom(List<String> userIds) {
+        for (String id : userIds) {
+            // create Peer
+
+            Peer peer = new Peer(_factory, iceServers, id, this);
+            peer.setOffer(false);
+            // add localStream
+            peer.addLocalStream(_localStream);
+            // 添加列表
+            peers.put(id, peer);
+        }
+        if (mCallback != null) {
+            mCallback.joinRoomSucc();
+        }
+
+        if (isHeadphonesPlugged()) {
+            toggleHeadset(true);
+        } else {
+            if (mIsAudioOnly)
+                toggleSpeaker(false);
+            else {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+                } else {
+                    audioManager.setMode(AudioManager.MODE_IN_CALL);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void userIn(String userId) {
+        // create Peer
+        Peer peer = new Peer(_factory, iceServers, userId, this);
+        peer.setOffer(true);
+        // add localStream
+        peer.addLocalStream(_localStream);
+        // 添加列表
+        peers.put(userId, peer);
+        // createOffer
+        peer.createOffer();
+    }
+
+    @Override
+    public void userReject(String userId, int type) {
+        //拒绝接听userId应该是没有添加进peers里去不需要remove
+//       Peer peer = peers.get(userId);
+//        if (peer != null) {
+//            peer.close();
+//            peers.remove(userId);
+//        }
+//        if (peers.size() == 0) {
+
+        if (mCallback != null) {
+            mCallback.reject(type);
+        }
+//        }
+    }
+
+    @Override
+    public void disconnected(String userId, EnumType.CallEndReason reason) {
+        if (mCallback != null) {
+            mCallback.disconnected(reason);
+        }
+    }
+
+    @Override
+    public void receiveOffer(String userId, String description) {
+        Peer peer = peers.get(userId);
+        if (peer != null) {
+            SessionDescription sdp = new SessionDescription(SessionDescription.Type.OFFER, description);
+            peer.setOffer(false);
+            peer.setRemoteDescription(sdp);
+            peer.createAnswer();
+        }
+
+
+    }
+
+    @Override
+    public void receiveAnswer(String userId, String sdp) {
+        Log.d("dds_test", "receiveAnswer--" + userId);
+        Peer peer = peers.get(userId);
+        if (peer != null) {
+            SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
+            peer.setRemoteDescription(sessionDescription);
+        }
+
+
+    }
+
+    @Override
+    public void receiveIceCandidate(String userId, String id, int label, String candidate) {
+        Log.d("dds_test", "receiveIceCandidate--" + userId);
+        Peer peer = peers.get(userId);
+        if (peer != null) {
+            IceCandidate iceCandidate = new IceCandidate(id, label, candidate);
+            peer.addRemoteIceCandidate(iceCandidate);
+
+        }
+    }
+
+    @Override
+    public void leaveRoom(String userId) {
+        Peer peer = peers.get(userId);
+        if (peer != null) {
+            peer.close();
+            peers.remove(userId);
+        }
+       Log.d(TAG, "leaveRoom peers.size() = " + peers.size() + "; mCallback = " + mCallback);
+        if (peers.size() <= 1) {
+
+            if (mCallback != null) {
+                mCallback.exitRoom();
+            }
+            if (peers.size() == 1) {
+                for (Map.Entry<String, Peer> set : peers.entrySet()) {
+                    set.getValue().close();
+                }
+                peers.clear();
+            }
+        }
+
+
+    }
+
+    @Override
+    public View startPreview(boolean isOverlay) {
+        if (mRootEglBase == null) {
+            return null;
+        }
+        localRenderer = new SurfaceViewRenderer(mContext);
+        localRenderer.init(mRootEglBase.getEglBaseContext(), null);
+        localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
+        localRenderer.setMirror(true);
+        localRenderer.setZOrderMediaOverlay(isOverlay);
+
+        localSink = new ProxyVideoSink();
+        localSink.setTarget(localRenderer);
+        if (_localStream.videoTracks.size() > 0) {
+            _localStream.videoTracks.get(0).addSink(localSink);
+        }
+        return localRenderer;
+    }
+
+    @Override
+    public void stopPreview() {
+        if (localSink != null) {
+            localSink.setTarget(null);
+            localSink = null;
+        }
+        if (audioSource != null) {
+            audioSource.dispose();
+            audioSource = null;
+        }
+        // 释放摄像头
+        if (captureAndroid != null) {
+            try {
+                captureAndroid.stopCapture();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            captureAndroid.dispose();
+            captureAndroid = null;
+        }
+        // 释放画布
+        if (surfaceTextureHelper != null) {
+            surfaceTextureHelper.dispose();
+            surfaceTextureHelper = null;
+        }
+
+        if (videoSource != null) {
+            videoSource.dispose();
+            videoSource = null;
+        }
+        if (_localStream != null) {
+            _localStream = null;
+        }
+        if (localRenderer != null) {
+            localRenderer.release();
+        }
+
+
+    }
+
+    @Override
+    public void startStream() {
+
+    }
+
+    @Override
+    public void stopStream() {
+
+    }
+
+
+    @Override
+    public View setupRemoteVideo(String userId, boolean isO) {
+        if (TextUtils.isEmpty(userId)) {
+            Log.e(TAG, "setupRemoteVideo userId is null ");
+            return null;
+        }
+        Peer peer = peers.get(userId);
+        if (peer == null) return null;
+
+        if (peer.renderer == null) {
+            peer.createRender(mRootEglBase, mContext, isO);
+        }
+
+        return peer.renderer;
+
+    }
+
+    @Override
+    public void stopRemoteVideo() {
+
+    }
+
+    private boolean isSwitch = false; // 是否正在切换摄像头
+
+    @Override
+    public void switchCamera() {
+        if (isSwitch) return;
+        isSwitch = true;
+        if (captureAndroid == null) return;
+        if (captureAndroid instanceof CameraVideoCapturer) {
+            CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) captureAndroid;
+            try {
+                cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() {
+                    @Override
+                    public void onCameraSwitchDone(boolean isFrontCamera) {
+                        isSwitch = false;
+                    }
+
+                    @Override
+                    public void onCameraSwitchError(String errorDescription) {
+                        isSwitch = false;
+                    }
+                });
+            } catch (Exception e) {
+                isSwitch = false;
+            }
+        } else {
+            Log.d(TAG, "Will not switch camera, video caputurer is not a camera");
+        }
+    }
+
+    @Override
+    public boolean muteAudio(boolean enable) {
+        if (_localAudioTrack != null) {
+            _localAudioTrack.setEnabled(!enable);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean toggleSpeaker(boolean enable) {
+        if (audioManager != null) {
+            isSpeakerOn = enable;
+            audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+            if (enable) {
+                audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+                        audioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL),
+                        AudioManager.FX_KEY_CLICK);
+                audioManager.setSpeakerphoneOn(true);
+            } else {
+                //5.0以上
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+                } else {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_CALL);
+                }
+                //设置音量,解决有些机型切换后没声音或者声音突然变大的问题
+                audioManager.setStreamVolume(
+                        AudioManager.STREAM_VOICE_CALL,
+                        audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL),
+                        AudioManager.FX_KEY_CLICK
+                );
+                audioManager.setSpeakerphoneOn(false);
+            }
+            return true;
+        }
+        return false;
+
+    }
+
+    @Override
+    public boolean toggleHeadset(boolean isHeadset) {
+        if (audioManager != null) {
+            if (isHeadset) {
+                //5.0以上
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+                } else {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_CALL);
+                }
+                audioManager.setSpeakerphoneOn(false);
+            } else {
+                if (mIsAudioOnly) {
+                    toggleSpeaker(isSpeakerOn);
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isHeadphonesPlugged() {
+        if (audioManager == null) {
+            return false;
+        }
+        if (Build.VERSION.SDK_INT >= 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();
+        }
+    }
+
+    @Override
+    public void release() {
+        if (audioManager != null) {
+            audioManager.setMode(AudioManager.MODE_NORMAL);
+        }
+        // 清空peer
+        if (peers != null) {
+            for (Peer peer : peers.values()) {
+                peer.close();
+            }
+            peers.clear();
+        }
+
+
+        // 停止预览
+        stopPreview();
+
+        if (_factory != null) {
+            _factory.dispose();
+            _factory = null;
+        }
+
+        if (mRootEglBase != null) {
+            mRootEglBase.release();
+            mRootEglBase = null;
+        }
+
+
+    }
+
+    // -----------------------------其他方法--------------------------------
+
+    private void initIceServer() {
+        // 初始化一些stun和turn的地址
+        PeerConnection.IceServer var1 = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302")
+                .createIceServer();
+        iceServers.add(var1);
+
+        PeerConnection.IceServer var11 = PeerConnection.IceServer.builder("stun:"+serverIP+":3478?transport=udp")
+                .createIceServer();
+        PeerConnection.IceServer var12 = PeerConnection.IceServer.builder("turn:"+serverIP+":3478?transport=udp")
+                .setUsername(turnUser)
+                .setPassword(turnUserPwd)
+                .createIceServer();
+        PeerConnection.IceServer var13 = PeerConnection.IceServer.builder("turn:"+serverIP+":3478?transport=tcp")
+                .setUsername(turnUser)
+                .setPassword(turnUserPwd)
+                .createIceServer();
+        iceServers.add(var11);
+        iceServers.add(var12);
+        iceServers.add(var13);
+    }
+
+    /**
+     * 构造PeerConnectionFactory
+     *
+     * @return PeerConnectionFactory
+     */
+    public PeerConnectionFactory createConnectionFactory() {
+
+        // 1. 初始化的方法,必须在开始之前调用
+        PeerConnectionFactory.initialize(PeerConnectionFactory
+                .InitializationOptions
+                .builder(mContext)
+                .createInitializationOptions());
+
+        // 2. 设置编解码方式:默认方法
+        final VideoEncoderFactory encoderFactory;
+        final VideoDecoderFactory decoderFactory;
+
+        encoderFactory = new DefaultVideoEncoderFactory(
+                mRootEglBase.getEglBaseContext(),
+                true,
+                true);
+        decoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext());
+
+        // 构造Factory
+        AudioDeviceModule audioDeviceModule = JavaAudioDeviceModule.builder(mContext).createAudioDeviceModule();
+        PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
+        return PeerConnectionFactory.builder()
+                .setOptions(options)
+                .setAudioDeviceModule(audioDeviceModule)
+                .setVideoEncoderFactory(encoderFactory)
+                .setVideoDecoderFactory(decoderFactory)
+                .createPeerConnectionFactory();
+    }
+
+    /**
+     * 创建本地流
+     */
+    public void createLocalStream() {
+        _localStream = _factory.createLocalMediaStream("ARDAMS");
+        // 音频
+        audioSource = _factory.createAudioSource(createAudioConstraints());
+        _localAudioTrack = _factory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
+        _localStream.addTrack(_localAudioTrack);
+
+        // 视频
+        if (!mIsAudioOnly) {
+            captureAndroid = createVideoCapture();
+            surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", mRootEglBase.getEglBaseContext());
+            videoSource = _factory.createVideoSource(captureAndroid.isScreencast());
+
+            captureAndroid.initialize(surfaceTextureHelper, mContext, videoSource.getCapturerObserver());
+            captureAndroid.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, FPS);
+
+
+            VideoTrack _localVideoTrack = _factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
+            _localStream.addTrack(_localVideoTrack);
+        }
+
+    }
+
+
+    // 是否使用录屏
+    private boolean screencaptureEnabled = false;
+
+    /**
+     * 创建媒体方式
+     *
+     * @return VideoCapturer
+     */
+    private VideoCapturer createVideoCapture() {
+        VideoCapturer videoCapturer;
+
+
+        if (screencaptureEnabled) {
+            return createScreenCapturer();
+        }
+
+        if (Camera2Enumerator.isSupported(mContext)) {
+            videoCapturer = createCameraCapture(new Camera2Enumerator(mContext));
+        } else {
+            videoCapturer = createCameraCapture(new Camera1Enumerator(true));
+        }
+        return videoCapturer;
+    }
+
+    /**
+     * 创建相机媒体流
+     */
+    private VideoCapturer createCameraCapture(CameraEnumerator enumerator) {
+        final String[] deviceNames = enumerator.getDeviceNames();
+
+        // First, try to find front facing camera
+        for (String deviceName : deviceNames) {
+            if (enumerator.isFrontFacing(deviceName)) {
+                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
+
+                if (videoCapturer != null) {
+                    return videoCapturer;
+                }
+            }
+        }
+
+        // Front facing camera not found, try something else
+        for (String deviceName : deviceNames) {
+            if (!enumerator.isFrontFacing(deviceName)) {
+                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
+
+                if (videoCapturer != null) {
+                    return videoCapturer;
+                }
+            }
+        }
+
+        return null;
+    }
+
+
+    private static Intent mediaProjectionPermissionResultData;
+    private static int mediaProjectionPermissionResultCode;
+
+    @TargetApi(21)
+    private VideoCapturer createScreenCapturer() {
+        if (mediaProjectionPermissionResultCode != Activity.RESULT_OK) {
+            return null;
+        }
+        return new ScreenCapturerAndroid(
+                mediaProjectionPermissionResultData, new MediaProjection.Callback() {
+            @Override
+            public void onStop() {
+                Log.e(TAG, "User revoked permission to capture the screen.");
+            }
+        });
+    }
+
+    //**************************************各种约束******************************************/
+    private static final String AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation";
+    private static final String AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT = "googAutoGainControl";
+    private static final String AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter";
+    private static final String AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression";
+
+    // 配置音频参数
+    private MediaConstraints createAudioConstraints() {
+        MediaConstraints audioConstraints = new MediaConstraints();
+        audioConstraints.mandatory.add(
+                new MediaConstraints.KeyValuePair(AUDIO_ECHO_CANCELLATION_CONSTRAINT, "true"));
+        audioConstraints.mandatory.add(
+                new MediaConstraints.KeyValuePair(AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT, "false"));
+        audioConstraints.mandatory.add(
+                new MediaConstraints.KeyValuePair(AUDIO_HIGH_PASS_FILTER_CONSTRAINT, "false"));
+        audioConstraints.mandatory.add(
+                new MediaConstraints.KeyValuePair(AUDIO_NOISE_SUPPRESSION_CONSTRAINT, "true"));
+        return audioConstraints;
+    }
+
+    //------------------------------------回调---------------------------------------------
+    @Override
+    public void onSendIceCandidate(String userId, IceCandidate candidate) {
+        if (mCallback != null) {
+            mCallback.onSendIceCandidate(userId, candidate);
+        }
+
+    }
+
+    @Override
+    public void onSendOffer(String userId, SessionDescription description) {
+        if (mCallback != null) {
+            mCallback.onSendOffer(userId, description);
+        }
+    }
+
+    @Override
+    public void onSendAnswer(String userId, SessionDescription description) {
+        if (mCallback != null) {
+            mCallback.onSendAnswer(userId, description);
+        }
+    }
+
+    @Override
+    public void onRemoteStream(String userId, MediaStream stream) {
+        if (mCallback != null) {
+            mCallback.onRemoteStream(userId);
+        }
+    }
+
+    @Override
+    public void onRemoveStream(String userId, MediaStream stream) {
+        leaveRoom(userId);
+    }
+
+    @Override
+    public void onDisconnected(String userId) {
+        if (mCallback != null) {
+           Log.d(TAG, "onDisconnected mCallback != null");
+            mCallback.onDisconnected(userId);
+        } else {
+           Log.d(TAG, "onDisconnected mCallback == null");
+        }
+    }
+
+}

+ 11 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/except/NotInitializedException.java

@@ -0,0 +1,11 @@
+package com.wdkl.webrtc.rtclib.except;
+
+/**
+ * Created by Allen.Fu on 17/02/2018.
+ */
+
+public class NotInitializedException extends RuntimeException {
+    public NotInitializedException() {
+        super("Not init!!!");
+    }
+}

+ 12 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/inter/ILogEvent.java

@@ -0,0 +1,12 @@
+package com.wdkl.webrtc.rtclib.inter;
+
+/**
+ * Created by Allen.Fu on 2020/2/10.
+ * 通话记录
+ * <p>
+ * 去电  来电  通话时长  通话类型  是否接听
+ */
+public interface ILogEvent {
+
+
+}

+ 47 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/inter/ISkyEvent.java

@@ -0,0 +1,47 @@
+package com.wdkl.webrtc.rtclib.inter;
+
+import java.util.List;
+
+/**
+ * Created by Allen.Fu on 2019/8/21.
+ * android_shuai@163.com
+ */
+public interface ISkyEvent {
+
+    // 创建房间
+    void createRoom(String room, int roomSize);
+
+    // 发送单人邀请
+    void sendInvite(String room, List<String> userIds, boolean audioOnly);
+
+    void sendRefuse(String room, String inviteId, int refuseType);
+
+    void sendTransAudio(String toId);
+
+    void sendDisConnect(String room, String toId, boolean isCrashed);
+
+    void sendCancel(String mRoomId, List<String> toId);
+
+    void sendJoin(String room);
+
+    void sendRingBack(String targetId, String room);
+
+    void sendLeave(String room, String userId);
+
+    // sendOffer
+    void sendOffer(String userId, String sdp);
+
+    // sendAnswer
+    void sendAnswer(String userId, String sdp);
+
+    // sendIceCandidate
+    void sendIceCandidate(String userId, String id, int label, String candidate);
+
+    void onRemoteRing();
+
+    void shouldStartRing(boolean isComing);
+
+    void shouldStopRing();
+
+
+}

+ 8 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/model/RoomInfo.java

@@ -0,0 +1,8 @@
+package com.wdkl.webrtc.rtclib.model;
+
+public class RoomInfo {
+
+    public String roomId;
+    public int roomSize;
+
+}

+ 18 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/model/UserInfo.java

@@ -0,0 +1,18 @@
+package com.wdkl.webrtc.rtclib.model;
+
+/**
+ * Created by Allen.Fu.
+ * 用户信息
+ */
+public class UserInfo {
+
+    private String userId;
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+}

+ 27 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/render/ProxyVideoSink.java

@@ -0,0 +1,27 @@
+package com.wdkl.webrtc.rtclib.render;
+
+import org.webrtc.Logging;
+import org.webrtc.VideoFrame;
+import org.webrtc.VideoSink;
+
+/**
+ * Created by Allen.Fu.
+ */
+public class ProxyVideoSink implements VideoSink {
+    private static final String TAG = "dds_ProxyVideoSink";
+    private VideoSink target;
+
+    @Override
+    synchronized public void onFrame(VideoFrame frame) {
+        if (target == null) {
+            Logging.d(TAG, "Dropping frame in proxy because target is null.");
+            return;
+        }
+        target.onFrame(frame);
+    }
+
+    synchronized public void setTarget(VideoSink target) {
+        this.target = target;
+    }
+
+}

+ 171 - 0
wdklRTC/src/main/java/com/wdkl/webrtc/rtclib/render/VideoFileRenderer.java

@@ -0,0 +1,171 @@
+/*
+ *  Copyright 2016 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+package com.wdkl.webrtc.rtclib.render;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import org.webrtc.EglBase;
+import org.webrtc.Logging;
+import org.webrtc.ThreadUtils;
+import org.webrtc.VideoFrame;
+import org.webrtc.VideoSink;
+import org.webrtc.YuvConverter;
+import org.webrtc.YuvHelper;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Can be used to save the video frames to file.
+ */
+public class VideoFileRenderer implements VideoSink {
+  private static final String TAG = "VideoFileRenderer";
+
+  private final HandlerThread renderThread;
+  private final Handler renderThreadHandler;
+  private final HandlerThread fileThread;
+  private final Handler fileThreadHandler;
+  private final FileOutputStream videoOutFile;
+  private final String outputFileName;
+  private final int outputFileWidth;
+  private final int outputFileHeight;
+  private final int outputFrameSize;
+  private final ByteBuffer outputFrameBuffer;
+  private EglBase eglBase;
+  private YuvConverter yuvConverter;
+  private int frameCount;
+
+  public VideoFileRenderer(String outputFile, int outputFileWidth, int outputFileHeight,
+                           final EglBase.Context sharedContext) throws IOException {
+    if ((outputFileWidth % 2) == 1 || (outputFileHeight % 2) == 1) {
+      throw new IllegalArgumentException("Does not support uneven width or height");
+    }
+
+    this.outputFileName = outputFile;
+    this.outputFileWidth = outputFileWidth;
+    this.outputFileHeight = outputFileHeight;
+
+    outputFrameSize = outputFileWidth * outputFileHeight * 3 / 2;
+    outputFrameBuffer = ByteBuffer.allocateDirect(outputFrameSize);
+
+    videoOutFile = new FileOutputStream(outputFile);
+    videoOutFile.write(
+        ("YUV4MPEG2 C420 W" + outputFileWidth + " H" + outputFileHeight + " Ip F30:1 A1:1\n")
+            .getBytes(Charset.forName("US-ASCII")));
+
+    renderThread = new HandlerThread(TAG + "RenderThread");
+    renderThread.start();
+    renderThreadHandler = new Handler(renderThread.getLooper());
+
+    fileThread = new HandlerThread(TAG + "FileThread");
+    fileThread.start();
+    fileThreadHandler = new Handler(fileThread.getLooper());
+
+    ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
+      @Override
+      public void run() {
+        eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
+        eglBase.createDummyPbufferSurface();
+        eglBase.makeCurrent();
+        yuvConverter = new YuvConverter();
+      }
+    });
+  }
+
+  @Override
+  public void onFrame(VideoFrame frame) {
+    frame.retain();
+    renderThreadHandler.post(() -> renderFrameOnRenderThread(frame));
+  }
+
+  private void renderFrameOnRenderThread(VideoFrame frame) {
+    final VideoFrame.Buffer buffer = frame.getBuffer();
+
+    // If the frame is rotated, it will be applied after cropAndScale. Therefore, if the frame is
+    // rotated by 90 degrees, swap width and height.
+    final int targetWidth = frame.getRotation() % 180 == 0 ? outputFileWidth : outputFileHeight;
+    final int targetHeight = frame.getRotation() % 180 == 0 ? outputFileHeight : outputFileWidth;
+
+    final float frameAspectRatio = (float) buffer.getWidth() / (float) buffer.getHeight();
+    final float fileAspectRatio = (float) targetWidth / (float) targetHeight;
+
+    // Calculate cropping to equalize the aspect ratio.
+    int cropWidth = buffer.getWidth();
+    int cropHeight = buffer.getHeight();
+    if (fileAspectRatio > frameAspectRatio) {
+      cropHeight = (int) (cropHeight * (frameAspectRatio / fileAspectRatio));
+    } else {
+      cropWidth = (int) (cropWidth * (fileAspectRatio / frameAspectRatio));
+    }
+
+    final int cropX = (buffer.getWidth() - cropWidth) / 2;
+    final int cropY = (buffer.getHeight() - cropHeight) / 2;
+
+    final VideoFrame.Buffer scaledBuffer =
+        buffer.cropAndScale(cropX, cropY, cropWidth, cropHeight, targetWidth, targetHeight);
+    frame.release();
+
+    final VideoFrame.I420Buffer i420 = scaledBuffer.toI420();
+    scaledBuffer.release();
+
+    fileThreadHandler.post(() -> {
+      YuvHelper.I420Rotate(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(),
+          i420.getDataV(), i420.getStrideV(), outputFrameBuffer, i420.getWidth(), i420.getHeight(),
+          frame.getRotation());
+      i420.release();
+
+      try {
+        videoOutFile.write("FRAME\n".getBytes(Charset.forName("US-ASCII")));
+        videoOutFile.write(
+            outputFrameBuffer.array(), outputFrameBuffer.arrayOffset(), outputFrameSize);
+      } catch (IOException e) {
+        throw new RuntimeException("Error writing video to disk", e);
+      }
+      frameCount++;
+    });
+  }
+
+  /**
+   * Release all resources. All already posted frames will be rendered first.
+   */
+  public void release() {
+    final CountDownLatch cleanupBarrier = new CountDownLatch(1);
+    renderThreadHandler.post(() -> {
+      yuvConverter.release();
+      eglBase.release();
+      renderThread.quit();
+      cleanupBarrier.countDown();
+    });
+    ThreadUtils.awaitUninterruptibly(cleanupBarrier);
+    fileThreadHandler.post(() -> {
+      try {
+        videoOutFile.close();
+        Logging.d(TAG,
+            "Video written to disk as " + outputFileName + ". The number of frames is " + frameCount
+                + " and the dimensions of the frames are " + outputFileWidth + "x"
+                + outputFileHeight + ".");
+      } catch (IOException e) {
+        throw new RuntimeException("Error closing output file", e);
+      }
+      fileThread.quit();
+    });
+    try {
+      fileThread.join();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      Logging.e(TAG, "Interrupted while waiting for the write to disk to complete.", e);
+    }
+  }
+}

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 20 - 0
wdklRTC/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
wdklRTC/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
wdklRTC/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
wdklRTC/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
wdklRTC/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
wdklRTC/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
wdklRTC/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
wdklRTC/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
wdklRTC/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
wdklRTC/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
wdklRTC/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>

+ 47 - 0
wdklRTC/src/main/res/layout/activity_launcher.xml

@@ -0,0 +1,47 @@
+<?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.webrtc.demo.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 " />
+
+        <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>

+ 32 - 0
wdklRTC/src/main/res/layout/activity_main.xml

@@ -0,0 +1,32 @@
+<?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:name="androidx.navigation.fragment.NavHostFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:defaultNavHost="true"
+        app:layout_constraintBottom_toTopOf="@id/nav_view"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:navGraph="@navigation/mobile_navigation" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 21 - 0
wdklRTC/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.webrtc.demo.core.voip.CallMultiActivity">
+
+    <FrameLayout
+        android:id="@+id/meeting_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="3"
+        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="2" />
+</LinearLayout>

+ 0 - 0
wdklRTC/src/main/res/layout/activity_single_call.xml


Some files were not shown because too many files changed in this diff