├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── vmloft │ │ └── develop │ │ └── app │ │ └── chat │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── vmloft │ │ └── develop │ │ └── app │ │ └── demo │ │ └── call │ │ ├── App.java │ │ ├── MainActivity.java │ │ ├── camera │ │ └── PreviewManager.java │ │ ├── conference │ │ ├── ConferenceActivity.java │ │ ├── ConferenceMemberView.java │ │ └── ConferenceViewAdapter.java │ │ ├── push │ │ └── MIPushReceiver.java │ │ └── single │ │ ├── CallActivity.java │ │ ├── CallEvent.java │ │ ├── CallManager.java │ │ ├── CallPushProvider.java │ │ ├── CallReceiver.java │ │ ├── CallStateListener.java │ │ ├── FloatWindow.java │ │ ├── VideoCallActivity.java │ │ └── VoiceCallActivity.java │ └── res │ ├── drawable-xxhdpi │ ├── ic_camera_change_24dp.png │ ├── ic_character_blackcat.png │ ├── ic_character_mustache.png │ ├── ic_character_penguin.png │ └── ic_character_spider.png │ ├── drawable │ ├── click_circle_green.xml │ ├── click_circle_red.xml │ ├── click_circle_transparent.xml │ ├── ic_add_24dp.xml │ ├── ic_call_24dp.xml │ ├── ic_call_end_24dp.xml │ ├── ic_camera_24dp.xml │ ├── ic_close_24dp.xml │ ├── ic_fullscreen_exit_24dp.xml │ ├── ic_info_outline_24dp.xml │ ├── ic_mic_off_24dp.xml │ ├── ic_record_24dp.xml │ ├── ic_screen_share_24dp.xml │ ├── ic_videocam_off_24dp.xml │ └── ic_volume_up_24dp.xml │ ├── layout │ ├── activity_conference.xml │ ├── activity_main.xml │ ├── activity_video_call.xml │ ├── activity_voice_call.xml │ ├── widget_conference_view.xml │ └── widget_float_window.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── raw │ ├── em_outgoing.ogg │ ├── sound_call_incoming.mp3 │ └── sound_calling.mp3 │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── screenshot-call-float-window-1.png ├── screenshot-call-float-window-2.png ├── screenshot-call-horizontal.png ├── screenshot-call.png └── screenshot-main.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | /local.properties 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | VMChatDemoCall 2 | -------------- 3 | 4 | 使用环信新版 SDK3.3.0以后版本实现完整的音视频通话功能,本次实现将所有的逻辑操作都放在了 VMCallManager 类中,方便对音视频界面最小化的管理; 5 | 此项目实现了音视频过界面的最小化,以及视频通话界面本地和远程画面的大小切换等功能 6 | 7 | ### #使用版本 8 | - AndroidStudio 3.1.2 9 | - Gradle 4.4 10 | - SDK Build Tools 27.0.3 11 | - SDK Compile 27 12 | - SDK mini 21 13 | - Design 27.1.1 14 | - [ButterKnife 8.8.1](https://github.com/JakeWharton/butterknife) 15 | - [EventBus 3.0.0](https://github.com/greenrobot/EventBus) 16 | - [环信 SDK 3.4.1](http://www.easemob.com/download/im) 17 | - [vmtools](https://github.com/lzan13/VMLibrary) 自己封装的工具类库 18 | 19 | >PS:这边并没有将 libs 目录上传到 github,需要大家自己去环信官网下载最新的 sdk 放在 libs 下 20 | 21 | 22 | ### #实现功能 23 | - 通话界面最小化及恢复 24 | - 通话悬浮窗的实现,可拖动 25 | - 视频通话界面切换 26 | - 视频通话的录制 27 | - 视频通话的截图 28 | - 横竖屏的自动切换 29 | - 呼叫对方离线时发送推送通知 30 | - 多人会议 31 | 32 | ### #已知问题 33 | - 未接通时切换到悬浮窗,当接通时无法显示画面 34 | 35 | ### #项目截图 36 | ![首界面](/screenshots/screenshot-main.png?raw=true "首界面") 37 | ![通话界面](/screenshots/screenshot-call.png?raw=true "通话界面") 38 | ![通话界面](/screenshots/screenshot-call-horizontal.png?raw=true "通话界面") 39 | ![悬浮窗](/screenshots/screenshot-call-float-window-1.png?raw=true "悬浮窗") 40 | ![悬浮窗](/screenshots/screenshot-call-float-window-2.png?raw=true "悬浮窗") 41 | 42 | ### #关联项目 43 | 实现有一个 TV 端的应用,可以实现和移动端进行实时通话,给大家在 TV 端使用环信 SDK 进行集成音视频通话加以参考 44 | 【[TV 端视频通话项目](https://github.com/lzan13/VMTVCall)】 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /libs 3 | /src/main/jniLibs 4 | /app.iml 5 | 6 | /config.properties 7 | /google-services.json 8 | 9 | *.apk 10 | *.jks -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | buildToolsVersion '27.0.3' 6 | 7 | defaultConfig { 8 | applicationId "com.vmloft.develop.app.demo.call" 9 | minSdkVersion 16 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName '0.1.0' 13 | } 14 | 15 | // 编译配置 16 | buildTypes { 17 | debug { 18 | // 简单粗暴解决多个库引用 jni 库出现 UnsatisfiedLinkError 文件错误问题 19 | ndk { 20 | abiFilters = ["armeabi-v7a"] 21 | } 22 | // 是否开启压缩 23 | zipAlignEnabled false 24 | // 是否开启混淆 25 | minifyEnabled false 26 | } 27 | release { 28 | minifyEnabled false 29 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 30 | } 31 | } 32 | sourceSets { 33 | main { 34 | // 设置Jni so文件路径 如果有jniLibs目录就不需要设置 35 | jniLibs.srcDirs = ['libs'] 36 | } 37 | } 38 | lintOptions { 39 | abortOnError false 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation fileTree(include: ['*.jar'], dir: 'libs') 45 | implementation 'com.android.support:design:27.1.1' 46 | // ButterKnife 库 47 | implementation 'com.jakewharton:butterknife:8.8.1' 48 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 49 | implementation 'org.greenrobot:eventbus:3.0.0' 50 | 51 | // implementation 'com.hyphenate:hyphenate-sdk:3.4.0.1' 52 | 53 | // 引用自己封装的工具类库,GitHub:https://github.com/lzan13/VMLibrary 54 | implementation 'com.vmloft.library:vmtools:0.0.2' 55 | } 56 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\develop\android\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/vmloft/develop/app/chat/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.chat; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 82 | 83 | 88 | 89 | 94 | 95 | 96 | 99 | 100 | 103 | 104 | 105 | 111 | 115 | 119 | 122 | 123 | 124 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/App.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.os.Handler; 8 | import android.os.Message; 9 | import android.widget.Toast; 10 | 11 | import com.hyphenate.EMConferenceListener; 12 | import com.hyphenate.EMConnectionListener; 13 | import com.hyphenate.EMError; 14 | import com.hyphenate.EMMessageListener; 15 | import com.hyphenate.chat.EMClient; 16 | import com.hyphenate.chat.EMConferenceStream; 17 | import com.hyphenate.chat.EMMessage; 18 | import com.hyphenate.chat.EMOptions; 19 | import com.hyphenate.chat.EMStreamStatistics; 20 | import com.vmloft.develop.app.demo.call.conference.ConferenceActivity; 21 | import com.vmloft.develop.app.demo.call.single.CallManager; 22 | import com.vmloft.develop.app.demo.call.single.CallReceiver; 23 | import com.vmloft.develop.library.tools.VMApp; 24 | import com.vmloft.develop.library.tools.VMTools; 25 | import com.vmloft.develop.library.tools.utils.VMLog; 26 | 27 | import java.util.Iterator; 28 | import java.util.List; 29 | 30 | /** 31 | * Created by lzan13 on 2016/5/25. 32 | *

33 | * 程序入口,做一些必要的初始化操作 34 | */ 35 | public class App extends VMApp { 36 | 37 | private CallReceiver callReceiver; 38 | 39 | @Override 40 | public void onCreate() { 41 | super.onCreate(); 42 | 43 | VMTools.init(context); 44 | // 初始化环信sdk 45 | initHyphenate(); 46 | } 47 | 48 | /** 49 | * 初始化环信sdk,并做一些注册监听的操作 50 | */ 51 | private void initHyphenate() { 52 | // 获取当前进程 id 并取得进程名 53 | int pid = android.os.Process.myPid(); 54 | String processAppName = getAppName(pid); 55 | /** 56 | * 如果app启用了远程的service,此application:onCreate会被调用2次 57 | * 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次 58 | * 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process name就立即返回 59 | */ 60 | if (processAppName == null || !processAppName.equalsIgnoreCase(context.getPackageName())) { 61 | // 则此application的onCreate 是被service 调用的,直接返回 62 | return; 63 | } 64 | 65 | // 初始化sdk的一些配置 66 | EMOptions options = new EMOptions(); 67 | // options.enableDNSConfig(false); 68 | // options.setIMServer("im1.easemob.cm"); 69 | // options.setImPort(443); 70 | // options.setRestServer("a1.easemob.com:80"); 71 | // options.setAppKey("easemob-demo#chatdemoui"); 72 | // options.setAppKey("easemob-demo#chatuidemo"); 73 | // options.setAppKey("hx-ps#api4vip6"); 74 | // options.setAppKey("cx-dev#cxstudy"); 75 | 76 | options.setAutoLogin(true); 77 | // 设置小米推送 appID 和 appKey 78 | // options.setMipushConfig("2882303761517573806", "5981757315806"); 79 | 80 | // 设置消息是否按照服务器时间排序 81 | options.setSortMessageByServerTime(false); 82 | 83 | // 初始化环信SDK,一定要先调用init() 84 | EMClient.getInstance().init(context, options); 85 | 86 | // 开启 debug 模式 87 | EMClient.getInstance().setDebugMode(true); 88 | 89 | // 设置通话广播监听器 90 | IntentFilter callFilter = new IntentFilter(EMClient.getInstance() 91 | .callManager() 92 | .getIncomingCallBroadcastAction()); 93 | if (callReceiver == null) { 94 | callReceiver = new CallReceiver(); 95 | } 96 | //注册通话广播接收者 97 | context.registerReceiver(callReceiver, callFilter); 98 | 99 | CallManager.getInstance().setExternalInputData(false); 100 | // 通话管理类的初始化 101 | CallManager.getInstance().init(context); 102 | 103 | setConnectionListener(); 104 | 105 | setConferenceListener(); 106 | 107 | setMessageListener(); 108 | } 109 | 110 | /** 111 | * 设置连接监听 112 | */ 113 | private void setConnectionListener() { 114 | EMConnectionListener connectionListener = new EMConnectionListener() { 115 | @Override 116 | public void onConnected() { 117 | 118 | } 119 | 120 | @Override 121 | public void onDisconnected(int i) { 122 | String str = "" + i; 123 | switch (i) { 124 | case EMError.USER_REMOVED: 125 | str = "账户被移除"; 126 | break; 127 | case EMError.USER_LOGIN_ANOTHER_DEVICE: 128 | str = "其他设备登录"; 129 | break; 130 | case EMError.USER_KICKED_BY_OTHER_DEVICE: 131 | str = "其他设备强制下线"; 132 | break; 133 | case EMError.USER_KICKED_BY_CHANGE_PASSWORD: 134 | str = "密码修改"; 135 | break; 136 | case EMError.SERVER_SERVICE_RESTRICTED: 137 | str = "被后台限制"; 138 | break; 139 | } 140 | VMLog.i("onDisconnected %s", str); 141 | } 142 | }; 143 | EMClient.getInstance().addConnectionListener(connectionListener); 144 | } 145 | 146 | /** 147 | * 设置多人会议监听 148 | */ 149 | private void setConferenceListener() { 150 | EMClient.getInstance() 151 | .conferenceManager() 152 | .addConferenceListener(new EMConferenceListener() { 153 | @Override 154 | public void onMemberJoined(String username) { 155 | VMLog.i("Joined username: %s", username); 156 | } 157 | 158 | @Override 159 | public void onMemberExited(String username) { 160 | VMLog.i("Exited username: %s", username); 161 | } 162 | 163 | @Override 164 | public void onStreamAdded(EMConferenceStream stream) { 165 | VMLog.i("Stream added streamId: %s, streamName: %s, memberName: %s, username: %s, extension: %s, videoOff: %b, mute: %b", stream 166 | .getStreamId(), stream.getStreamName(), stream.getMemberName(), stream.getUsername(), stream 167 | .getExtension(), stream.isVideoOff(), stream.isAudioOff()); 168 | VMLog.i("Conference stream subscribable: %d, subscribed: %d", EMClient.getInstance() 169 | .conferenceManager() 170 | .getAvailableStreamMap() 171 | .size(), EMClient.getInstance() 172 | .conferenceManager() 173 | .getSubscribedStreamMap() 174 | .size()); 175 | } 176 | 177 | @Override 178 | public void onStreamRemoved(EMConferenceStream stream) { 179 | VMLog.i("Stream removed streamId: %s, streamName: %s, memberName: %s, username: %s, extension: %s, videoOff: %b, mute: %b", stream 180 | .getStreamId(), stream.getStreamName(), stream.getMemberName(), stream.getUsername(), stream 181 | .getExtension(), stream.isVideoOff(), stream.isAudioOff()); 182 | VMLog.i("Conference stream subscribable: %d, subscribed: %d", EMClient.getInstance() 183 | .conferenceManager() 184 | .getAvailableStreamMap() 185 | .size(), EMClient.getInstance() 186 | .conferenceManager() 187 | .getSubscribedStreamMap() 188 | .size()); 189 | } 190 | 191 | @Override 192 | public void onStreamUpdate(EMConferenceStream stream) { 193 | VMLog.i("Stream added streamId: %s, streamName: %s, memberName: %s, username: %s, extension: %s, videoOff: %b, mute: %b", stream 194 | .getStreamId(), stream.getStreamName(), stream.getMemberName(), stream.getUsername(), stream 195 | .getExtension(), stream.isVideoOff(), stream.isAudioOff()); 196 | VMLog.i("Conference stream subscribable: %d, subscribed: %d", EMClient.getInstance() 197 | .conferenceManager() 198 | .getAvailableStreamMap() 199 | .size(), EMClient.getInstance() 200 | .conferenceManager() 201 | .getSubscribedStreamMap() 202 | .size()); 203 | } 204 | 205 | @Override 206 | public void onPassiveLeave(int error, String message) { 207 | VMLog.i("passive leave code: %d, message: %s", error, message); 208 | } 209 | 210 | @Override 211 | public void onConferenceState(ConferenceState state) { 212 | VMLog.i("State " + state); 213 | } 214 | 215 | @Override 216 | public void onStreamStatistics(EMStreamStatistics emStreamStatistics) { 217 | VMLog.i(emStreamStatistics.toString()); 218 | } 219 | 220 | @Override 221 | public void onStreamSetup(String streamId) { 222 | VMLog.i("Stream id %s", streamId); 223 | } 224 | 225 | @Override 226 | public void onSpeakers(List list) { 227 | 228 | } 229 | 230 | @Override 231 | public void onReceiveInvite(String confId, String password, String extension) { 232 | VMLog.i("Receive conference invite confId: %s, password: %s, extension: %s", confId, password, extension); 233 | 234 | Intent conferenceIntent = new Intent(context, ConferenceActivity.class); 235 | conferenceIntent.putExtra("isCreator", false); 236 | conferenceIntent.putExtra("confId", confId); 237 | conferenceIntent.putExtra("password", password); 238 | conferenceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 239 | context.startActivity(conferenceIntent); 240 | } 241 | }); 242 | } 243 | 244 | private void setMessageListener() { 245 | EMClient.getInstance().chatManager().addMessageListener(new EMMessageListener() { 246 | @Override 247 | public void onMessageReceived(List list) { 248 | Message msg = handler.obtainMessage(0); 249 | msg.obj = list.get(0).toString(); 250 | handler.sendMessage(msg); 251 | for (EMMessage message : list) { 252 | VMLog.i("收到新消息" + message); 253 | } 254 | } 255 | 256 | @Override 257 | public void onCmdMessageReceived(List list) { 258 | 259 | } 260 | 261 | @Override 262 | public void onMessageRead(List list) { 263 | 264 | } 265 | 266 | @Override 267 | public void onMessageDelivered(List list) { 268 | 269 | } 270 | 271 | @Override 272 | public void onMessageRecalled(List list) { 273 | 274 | } 275 | 276 | @Override 277 | public void onMessageChanged(EMMessage emMessage, Object o) { 278 | 279 | } 280 | }); 281 | } 282 | 283 | Handler handler = new Handler() { 284 | @Override 285 | public void handleMessage(Message msg) { 286 | String str = (String) msg.obj; 287 | switch (msg.what) { 288 | case 0: 289 | Toast.makeText(context, str, Toast.LENGTH_SHORT).show(); 290 | break; 291 | } 292 | } 293 | }; 294 | 295 | /** 296 | * 根据Pid获取当前进程的名字,一般就是当前app的包名 297 | * 298 | * @param pid 进程的id 299 | * @return 返回进程的名字 300 | */ 301 | private String getAppName(int pid) { 302 | String processName = null; 303 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 304 | List list = activityManager.getRunningAppProcesses(); 305 | Iterator i = list.iterator(); 306 | while (i.hasNext()) { 307 | ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next()); 308 | try { 309 | if (info.pid == pid) { 310 | // 根据进程的信息获取当前进程的名字 311 | processName = info.processName; 312 | // 返回当前进程名 313 | return processName; 314 | } 315 | } catch (Exception e) { 316 | e.printStackTrace(); 317 | } 318 | } 319 | // 没有匹配的项,返回为null 320 | return processName; 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.view.ContextThemeWrapper; 6 | import android.view.View; 7 | import android.widget.Button; 8 | import android.widget.EditText; 9 | import android.widget.TextView; 10 | 11 | import butterknife.BindView; 12 | import butterknife.ButterKnife; 13 | 14 | import com.hyphenate.EMCallBack; 15 | import com.hyphenate.chat.EMClient; 16 | import com.hyphenate.chat.EMMessage; 17 | import com.hyphenate.exceptions.HyphenateException; 18 | import com.vmloft.develop.app.demo.call.conference.ConferenceActivity; 19 | import com.vmloft.develop.app.demo.call.single.CallManager; 20 | import com.vmloft.develop.app.demo.call.single.VideoCallActivity; 21 | import com.vmloft.develop.app.demo.call.single.VoiceCallActivity; 22 | import com.vmloft.develop.library.tools.VMActivity; 23 | import com.vmloft.develop.library.tools.utils.VMLog; 24 | import com.vmloft.develop.library.tools.utils.VMSPUtil; 25 | import com.vmloft.develop.library.tools.widget.VMViewGroup; 26 | 27 | import org.json.JSONException; 28 | import org.json.JSONObject; 29 | 30 | 31 | /** 32 | * 音视频项目主类 33 | */ 34 | public class MainActivity extends VMActivity { 35 | 36 | @BindView(R.id.layout_root) View rootView; 37 | @BindView(R.id.view_group) VMViewGroup viewGroup; 38 | 39 | @BindView(R.id.edit_username) EditText usernameView; 40 | @BindView(R.id.edit_password) EditText passwordView; 41 | @BindView(R.id.edit_contacts_username) EditText contactsView; 42 | @BindView(R.id.text_info) TextView infoView; 43 | 44 | private String username; 45 | private String password; 46 | private String toUsername; 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | setContentView(R.layout.activity_main); 52 | 53 | ButterKnife.bind(activity); 54 | 55 | init(); 56 | } 57 | 58 | private void init() { 59 | username = (String) VMSPUtil.get("username", ""); 60 | password = (String) VMSPUtil.get("password", ""); 61 | toUsername = (String) VMSPUtil.get("toUsername", ""); 62 | usernameView.setText(username); 63 | passwordView.setText(password); 64 | contactsView.setText(toUsername); 65 | 66 | String[] btnTitle = {"登录", "注册", "退出", "语音呼叫", "视频呼叫", "发起会议", "发送消息", "新版推送"}; 67 | 68 | for (int i = 0; i < btnTitle.length; i++) { 69 | Button btn = new Button(new ContextThemeWrapper(activity, R.style.VMBtn_Green), null, 0); 70 | btn.setText(btnTitle[i]); 71 | btn.setId(100 + i); 72 | btn.setOnClickListener(viewListener); 73 | viewGroup.addView(btn); 74 | } 75 | 76 | } 77 | 78 | private View.OnClickListener viewListener = new View.OnClickListener() { 79 | @Override 80 | public void onClick(View v) { 81 | switch (v.getId()) { 82 | case 100: 83 | signIn(); 84 | break; 85 | case 101: 86 | signUp(); 87 | break; 88 | case 102: 89 | signOut(); 90 | break; 91 | case 103: 92 | callVoice(); 93 | break; 94 | case 104: 95 | callVideo(); 96 | break; 97 | case 105: 98 | videoConference(true); 99 | break; 100 | case 106: 101 | sendMessage(); 102 | break; 103 | case 107: 104 | sendNewPushMessage(); 105 | break; 106 | } 107 | } 108 | }; 109 | 110 | /** 111 | * 登录 112 | */ 113 | private void signIn() { 114 | username = usernameView.getText().toString().trim(); 115 | password = passwordView.getText().toString().trim(); 116 | if (username.isEmpty() || password.isEmpty()) { 117 | printInfo("username or password null"); 118 | return; 119 | } 120 | EMClient.getInstance().login(username, password, new EMCallBack() { 121 | @Override 122 | public void onSuccess() { 123 | VMLog.i("login success"); 124 | 125 | VMSPUtil.put("username", username); 126 | VMSPUtil.put("password", password); 127 | printInfo("login success"); 128 | } 129 | 130 | @Override 131 | public void onError(final int i, final String s) { 132 | String errorMsg = "login error: " + i + "; " + s; 133 | VMLog.i(errorMsg); 134 | printInfo(errorMsg); 135 | } 136 | 137 | @Override 138 | public void onProgress(int i, String s) { 139 | 140 | } 141 | }); 142 | } 143 | 144 | /** 145 | * 注册账户 146 | */ 147 | private void signUp() { 148 | username = usernameView.getText().toString().trim(); 149 | password = passwordView.getText().toString().trim(); 150 | if (username.isEmpty() || password.isEmpty()) { 151 | printInfo("username or password null"); 152 | return; 153 | } 154 | new Thread(new Runnable() { 155 | @Override 156 | public void run() { 157 | try { 158 | EMClient.getInstance().createAccount(username, password); 159 | } catch (HyphenateException e) { 160 | String errorMsg = "sign up error " + e.getErrorCode() + "; " + e.getMessage(); 161 | VMLog.d(errorMsg); 162 | printInfo(errorMsg); 163 | e.printStackTrace(); 164 | } 165 | } 166 | }).start(); 167 | } 168 | 169 | /** 170 | * 退出登录 171 | */ 172 | private void signOut() { 173 | EMClient.getInstance().logout(EMClient.getInstance().isConnected(), new EMCallBack() { 174 | @Override 175 | public void onSuccess() { 176 | VMLog.i("logout success"); 177 | printInfo("logout success"); 178 | } 179 | 180 | @Override 181 | public void onError(int i, String s) { 182 | String errorMsg = "logout error: " + i + "; " + s; 183 | VMLog.i(errorMsg); 184 | printInfo(errorMsg); 185 | } 186 | 187 | @Override 188 | public void onProgress(int i, String s) { 189 | 190 | } 191 | }); 192 | } 193 | 194 | /** 195 | * 视频呼叫 196 | */ 197 | private void callVideo() { 198 | checkContacts(); 199 | Intent intent = new Intent(MainActivity.this, VideoCallActivity.class); 200 | CallManager.getInstance().setChatId(toUsername); 201 | CallManager.getInstance().setInComingCall(false); 202 | CallManager.getInstance().setCallType(CallManager.CallType.VIDEO); 203 | startActivity(intent); 204 | } 205 | 206 | /** 207 | * 语音呼叫 208 | */ 209 | private void callVoice() { 210 | checkContacts(); 211 | Intent intent = new Intent(MainActivity.this, VoiceCallActivity.class); 212 | CallManager.getInstance().setChatId(toUsername); 213 | CallManager.getInstance().setInComingCall(false); 214 | CallManager.getInstance().setCallType(CallManager.CallType.VOICE); 215 | startActivity(intent); 216 | } 217 | 218 | private void checkContacts() { 219 | toUsername = contactsView.getText().toString().trim(); 220 | if (toUsername.isEmpty()) { 221 | printInfo("contact user not null"); 222 | return; 223 | } 224 | VMSPUtil.put("toUsername", toUsername); 225 | } 226 | 227 | /** 228 | * 发送消息 229 | */ 230 | private void sendMessage() { 231 | checkContacts(); 232 | EMMessage message = EMMessage.createTxtSendMessage("测试发送消息,主要是为了测试是否在线", toUsername); 233 | //设置强制推送 234 | message.setAttribute("em_force_notification", "true"); 235 | //设置自定义推送提示 236 | JSONObject extObj = new JSONObject(); 237 | try { 238 | extObj.put("em_push_title", "老版本推送显示内容"); 239 | extObj.put("extern", "定义推送扩展内容"); 240 | } catch (JSONException e) { 241 | e.printStackTrace(); 242 | } 243 | message.setAttribute("em_apns_ext", extObj); 244 | sendMessage(message); 245 | } 246 | 247 | /** 248 | * 发送新版推送消息 249 | */ 250 | private void sendNewPushMessage() { 251 | checkContacts(); 252 | EMMessage message = EMMessage.createTxtSendMessage("测试发送消息,主要是为了测试是否在线", toUsername); 253 | //设置强制推送 254 | message.setAttribute("em_force_notification", "true"); 255 | //设置自定义推送提示 256 | JSONObject extObj = new JSONObject(); 257 | try { 258 | extObj.put("em_push_name", "新版推送标题"); 259 | extObj.put("em_push_content", "新版推送显示内容"); 260 | extObj.put("extern", "定义推送扩展内容"); 261 | } catch (JSONException e) { 262 | e.printStackTrace(); 263 | } 264 | message.setAttribute("em_apns_ext", extObj); 265 | sendMessage(message); 266 | } 267 | 268 | /** 269 | * 最终调用发送信息方法 270 | * 271 | * @param message 需要发送的消息 272 | */ 273 | private void sendMessage(final EMMessage message) { 274 | /** 275 | * 调用sdk的消息发送方法发送消息,发送消息时要尽早的设置消息监听,防止消息状态已经回调, 276 | * 但是自己没有注册监听,导致检测不到消息状态的变化 277 | * 所以这里在发送之前先设置消息的状态回调 278 | */ 279 | message.setMessageStatusCallback(new EMCallBack() { 280 | @Override 281 | public void onSuccess() { 282 | String str = String.format("消息发送成功 msgId %s, content %s", message.getMsgId(), message 283 | .getBody()); 284 | VMLog.i(str); 285 | printInfo(str); 286 | } 287 | 288 | @Override 289 | public void onError(final int i, final String s) { 290 | String str = String.format("消息发送失败 code: %d, error: %s", i, s); 291 | VMLog.i(str); 292 | printInfo(str); 293 | } 294 | 295 | @Override 296 | public void onProgress(int i, String s) { 297 | // TODO 消息发送进度,这里不处理,留给消息Item自己去更新 298 | VMLog.i("消息发送中 progress: %d, %s", i, s); 299 | } 300 | }); 301 | // 发送消息 302 | EMClient.getInstance().chatManager().sendMessage(message); 303 | } 304 | 305 | /** 306 | * 发起视频会议 307 | */ 308 | private void videoConference(boolean isCreator) { 309 | Intent intent = new Intent(activity, ConferenceActivity.class); 310 | intent.putExtra("isCreator", isCreator); 311 | intent.putExtra("username", toUsername); 312 | onStartActivity(activity, intent); 313 | } 314 | 315 | private void printInfo(final String info) { 316 | runOnUiThread(new Runnable() { 317 | @Override 318 | public void run() { 319 | infoView.setText(infoView.getText().toString() + info + "\n"); 320 | } 321 | }); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/camera/PreviewManager.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.camera; 2 | 3 | import android.graphics.ImageFormat; 4 | import android.hardware.Camera; 5 | import android.hardware.Camera.Parameters; 6 | import android.hardware.Camera.PreviewCallback; 7 | import android.hardware.Camera.Size; 8 | import android.view.SurfaceHolder; 9 | import android.view.SurfaceView; 10 | 11 | import com.hyphenate.chat.EMClient; 12 | 13 | import java.io.IOException; 14 | import java.util.List; 15 | 16 | /** 17 | * Created by lzan13 on 2018/4/24. 18 | */ 19 | public class PreviewManager implements PreviewCallback { 20 | 21 | private Camera camera; 22 | private PreviewCallback previewCallback; 23 | private SurfaceView surfaceView; 24 | 25 | private int videoWidth = 320; 26 | private int videoHeight = 240; 27 | 28 | public PreviewManager(SurfaceView surfaceView) { 29 | this.surfaceView = surfaceView; 30 | init(); 31 | } 32 | 33 | private void init() { 34 | previewCallback = this; 35 | surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { 36 | @Override 37 | public void surfaceCreated(SurfaceHolder holder) { 38 | 39 | } 40 | 41 | @Override 42 | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 43 | // SurfaceView 构建完毕并且获取有效宽高时候打开摄像头 44 | if (camera != null) { 45 | return; 46 | } 47 | camera = Camera.open(); 48 | Parameters parms = camera.getParameters(); 49 | List listsize = parms.getSupportedPictureSizes(); 50 | Size sizeOut = null; 51 | for (Size size : listsize) { 52 | if (size.width >= width) { 53 | break; 54 | } 55 | sizeOut = size; 56 | } 57 | 58 | parms.setPreviewSize(videoWidth, videoHeight); 59 | parms.setPictureSize(videoWidth, videoHeight); 60 | parms.setPreviewFormat(ImageFormat.NV21); 61 | camera.setParameters(parms); 62 | camera.setPreviewCallback(previewCallback); 63 | try { 64 | camera.setPreviewDisplay(holder); 65 | camera.startPreview();//开始预览 66 | } catch (IOException e) { 67 | // TODO Auto-generated catch block 68 | e.printStackTrace(); 69 | } 70 | } 71 | 72 | @Override 73 | public void surfaceDestroyed(SurfaceHolder holder) { 74 | // 结束预览时关闭摄像头 75 | if (camera == null) { 76 | return; 77 | } 78 | camera.setPreviewCallback(null); 79 | camera.stopPreview();// 停止预览 80 | camera.release(); 81 | camera = null; 82 | } 83 | }); 84 | 85 | } 86 | 87 | @Override 88 | public void onPreviewFrame(byte[] data, Camera camera) { 89 | EMClient.getInstance() 90 | .callManager() 91 | .inputExternalVideoData(data, videoWidth, videoHeight, 0); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/conference/ConferenceMemberView.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.conference; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.widget.ImageView; 8 | import android.widget.RelativeLayout; 9 | import android.widget.TextView; 10 | 11 | import com.hyphenate.media.EMCallSurfaceView; 12 | import com.superrtc.sdk.VideoView; 13 | import com.vmloft.develop.app.demo.call.R; 14 | 15 | /** 16 | * Created by lzan13 on 2017/8/21. 17 | */ 18 | public class ConferenceMemberView extends RelativeLayout { 19 | 20 | private Context context; 21 | 22 | private EMCallSurfaceView surfaceView; 23 | private ImageView avatarView; 24 | private ImageView audioOffView; 25 | private ImageView talkingView; 26 | private TextView nameView; 27 | 28 | private boolean isVideoOff = true; 29 | private boolean isAudioOff = false; 30 | private boolean isDesktop = false; 31 | private String streamId; 32 | 33 | 34 | public ConferenceMemberView(Context context) { 35 | this(context, null); 36 | } 37 | 38 | public ConferenceMemberView(Context context, AttributeSet attrs) { 39 | this(context, attrs, 0); 40 | } 41 | 42 | public ConferenceMemberView(Context context, AttributeSet attrs, int defStyleAttr) { 43 | super(context, attrs, defStyleAttr); 44 | this.context = context; 45 | LayoutInflater.from(context).inflate(R.layout.widget_conference_view, this); 46 | init(); 47 | } 48 | 49 | private void init() { 50 | surfaceView = (EMCallSurfaceView) findViewById(R.id.item_surface_view); 51 | avatarView = (ImageView) findViewById(R.id.img_call_avatar); 52 | audioOffView = (ImageView) findViewById(R.id.icon_mute); 53 | talkingView = (ImageView) findViewById(R.id.icon_talking); 54 | nameView = (TextView) findViewById(R.id.text_name); 55 | 56 | surfaceView.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFit); 57 | } 58 | 59 | public EMCallSurfaceView getSurfaceView() { 60 | return surfaceView; 61 | } 62 | 63 | /** 64 | * 更新静音状态 65 | */ 66 | public void setAudioOff(boolean state) { 67 | if (isDesktop) { 68 | return; 69 | } 70 | isAudioOff = state; 71 | if (isAudioOff) { 72 | audioOffView.setVisibility(View.VISIBLE); 73 | } else { 74 | audioOffView.setVisibility(View.GONE); 75 | } 76 | } 77 | 78 | public boolean isAudioOff() { 79 | return isAudioOff; 80 | } 81 | 82 | /** 83 | * 更新视频显示状态 84 | */ 85 | public void setVideoOff(boolean state) { 86 | isVideoOff = state; 87 | if (isVideoOff) { 88 | avatarView.setVisibility(View.VISIBLE); 89 | } else { 90 | avatarView.setVisibility(View.GONE); 91 | } 92 | } 93 | 94 | public boolean isVideoOff() { 95 | return isVideoOff; 96 | } 97 | 98 | public void setDesktop(boolean desktop) { 99 | isDesktop = desktop; 100 | avatarView.setVisibility(View.GONE); 101 | } 102 | 103 | /** 104 | * 更新说话状态 105 | */ 106 | public void setTalking(boolean talking) { 107 | if (isDesktop) { 108 | return; 109 | } 110 | if (talking) { 111 | talkingView.setVisibility(VISIBLE); 112 | } else { 113 | talkingView.setVisibility(GONE); 114 | } 115 | } 116 | 117 | /** 118 | * 设置当前 view 对应的 stream 的用户,主要用来语音通话时显示对方头像 119 | */ 120 | public void setUsername(String username) { 121 | nameView.setText(username); 122 | } 123 | 124 | /** 125 | * 设置当前控件显示的 Stream Id 126 | */ 127 | public void setStreamId(String streamId) { 128 | this.streamId = streamId; 129 | } 130 | 131 | public String getStreamId() { 132 | return streamId; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/conference/ConferenceViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.conference; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import com.hyphenate.chat.EMConferenceStream; 9 | import com.hyphenate.media.EMCallSurfaceView; 10 | import com.superrtc.sdk.VideoView; 11 | import com.vmloft.develop.app.demo.call.R; 12 | import java.util.List; 13 | 14 | /** 15 | * Created by lzan13 on 2017/8/17. 16 | * 多人会议 UI 界面适配器 17 | */ 18 | public class ConferenceViewAdapter extends RecyclerView.Adapter { 19 | 20 | private Context context; 21 | private ConferenceCallItemClickListener clickListener; 22 | 23 | private List streamList; 24 | 25 | public ConferenceViewAdapter(Context context, List list) { 26 | this.context = context; 27 | streamList = list; 28 | } 29 | 30 | @Override public ConferenceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 31 | View view = LayoutInflater.from(context).inflate(R.layout.widget_conference_view, null); 32 | return new ConferenceViewHolder(view); 33 | } 34 | 35 | @Override public void onBindViewHolder(ConferenceViewHolder holder, final int position) { 36 | holder.surfaceView.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFill); 37 | holder.itemView.setOnClickListener(new View.OnClickListener() { 38 | @Override public void onClick(View view) { 39 | if (clickListener != null) { 40 | clickListener.onItemClick(position, view); 41 | } 42 | } 43 | }); 44 | } 45 | 46 | @Override public int getItemCount() { 47 | return streamList.size(); 48 | } 49 | 50 | /** 51 | * 设置点击监听接口实现 52 | */ 53 | public void setConferenceCallItemClickListener(ConferenceCallItemClickListener listener) { 54 | clickListener = listener; 55 | } 56 | 57 | /** 58 | * 定义 Item 点击监听接口 59 | */ 60 | public interface ConferenceCallItemClickListener { 61 | public void onItemClick(int position, View view); 62 | } 63 | 64 | static class ConferenceViewHolder extends RecyclerView.ViewHolder { 65 | 66 | EMCallSurfaceView surfaceView; 67 | 68 | public ConferenceViewHolder(View itemView) { 69 | super(itemView); 70 | surfaceView = (EMCallSurfaceView) itemView.findViewById(R.id.item_surface_view); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/push/MIPushReceiver.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.push; 2 | 3 | import android.content.Context; 4 | import com.hyphenate.chat.EMMipushReceiver; 5 | import com.vmloft.develop.library.tools.utils.VMLog; 6 | import com.xiaomi.mipush.sdk.ErrorCode; 7 | import com.xiaomi.mipush.sdk.MiPushClient; 8 | import com.xiaomi.mipush.sdk.MiPushCommandMessage; 9 | import com.xiaomi.mipush.sdk.MiPushMessage; 10 | import java.util.List; 11 | 12 | /** 13 | * Created by lzan13 on 2017/5/5. 14 | * 集成小米推送相关推送广播接收器,处理推送相关数据信息,这里只是接受环信离线消息通知,不做任何处理 15 | */ 16 | public class MIPushReceiver extends EMMipushReceiver { 17 | // 当前账户的 regId 18 | private String regId = null; 19 | 20 | /** 21 | * 接收客户端向服务器发送注册命令消息后返回的响应 22 | * 23 | * @param context 上下文对象 24 | * @param miPushCommandMessage 注册结果 25 | */ 26 | @Override public void onReceiveRegisterResult(Context context, MiPushCommandMessage miPushCommandMessage) { 27 | super.onReceiveRegisterResult(context, miPushCommandMessage); 28 | String command = miPushCommandMessage.getCommand(); 29 | List arguments = miPushCommandMessage.getCommandArguments(); 30 | String cmdArg1 = null; 31 | String cmdArg2 = null; 32 | if (arguments != null && arguments.size() > 0) { 33 | cmdArg1 = arguments.get(0); 34 | } 35 | if (arguments != null && arguments.size() > 1) { 36 | cmdArg2 = arguments.get(1); 37 | } 38 | if (MiPushClient.COMMAND_REGISTER.equals(command)) { 39 | if (miPushCommandMessage.getResultCode() == ErrorCode.SUCCESS) { 40 | // 这里可以获取到当前账户的 regId,可以发送给自己的服务器,用来做一些业务处理 41 | regId = cmdArg1; 42 | } 43 | } 44 | VMLog.d("onReceiveRegisterResult regId: %s", regId); 45 | } 46 | 47 | /** 48 | * 接收服务器推送的透传消息 49 | * 50 | * @param context 上下文对象 51 | * @param miPushMessage 推送消息对象 52 | */ 53 | @Override public void onReceivePassThroughMessage(Context context, MiPushMessage miPushMessage) { 54 | super.onReceivePassThroughMessage(context, miPushMessage); 55 | } 56 | 57 | /** 58 | * 接收服务器推送的通知栏消息(消息到达客户端时触发,并且可以接收应用在前台时不弹出通知的通知消息) 59 | * 60 | * @param context 上下文 61 | * @param miPushMessage 推送消息对象 62 | */ 63 | @Override public void onNotificationMessageArrived(Context context, MiPushMessage miPushMessage) { 64 | miPushMessage.setTitle("这里是客户端设置 title"); 65 | super.onNotificationMessageArrived(context, miPushMessage); 66 | } 67 | 68 | /** 69 | * 接收服务器发来的通知栏消息(用户点击通知栏时触发) 70 | * 71 | * @param context 上下文对象 72 | * @param miPushMessage 消息对象 73 | */ 74 | @Override public void onNotificationMessageClicked(Context context, MiPushMessage miPushMessage) { 75 | super.onNotificationMessageClicked(context, miPushMessage); 76 | } 77 | 78 | /** 79 | * 接收客户端向服务器发送命令消息后返回的响应 80 | * 81 | * @param context 上下文对象 82 | * @param miPushCommandMessage 服务器响应的命令消息对象 83 | */ 84 | @Override public void onCommandResult(Context context, MiPushCommandMessage miPushCommandMessage) { 85 | super.onCommandResult(context, miPushCommandMessage); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/CallActivity.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import android.content.Context; 4 | import android.os.Bundle; 5 | import android.os.Vibrator; 6 | import android.view.WindowManager; 7 | import com.hyphenate.chat.EMClient; 8 | import com.vmloft.develop.library.tools.VMActivity; 9 | import org.greenrobot.eventbus.EventBus; 10 | 11 | /** 12 | * Created by lzan13 on 2016/8/8. 13 | * 14 | * 通话界面的父类,做一些音视频通话的通用操作 15 | */ 16 | public class CallActivity extends VMActivity { 17 | 18 | // 呼叫方名字 19 | protected String chatId; 20 | 21 | // 震动器 22 | private Vibrator vibrator; 23 | 24 | @Override protected void onCreate(Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | // 设置通话界面属性,保持屏幕常亮,关闭输入法,以及解锁 27 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON 28 | | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD 29 | | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 30 | | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); 31 | } 32 | 33 | /** 34 | * 初始化界面方法,做一些界面的初始化操作 35 | */ 36 | protected void initView() { 37 | activity = this; 38 | 39 | initCallPushProvider(); 40 | 41 | // 初始化振动器 42 | vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE); 43 | 44 | if (CallManager.getInstance().getCallState() == CallManager.CallState.DISCONNECTED) { 45 | // 收到呼叫或者呼叫对方时初始化通话状态监听 46 | CallManager.getInstance().setCallState(CallManager.CallState.CONNECTING); 47 | CallManager.getInstance().registerCallStateListener(); 48 | CallManager.getInstance().attemptPlayCallSound(); 49 | 50 | // 如果不是对方打来的,就主动呼叫 51 | if (!CallManager.getInstance().isInComingCall()) { 52 | CallManager.getInstance().makeCall(); 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * 初始化通话推送提供者 59 | */ 60 | private void initCallPushProvider() { 61 | CallPushProvider pushProvider = new CallPushProvider(); 62 | EMClient.getInstance().callManager().setPushProvider(pushProvider); 63 | } 64 | 65 | /** 66 | * 挂断通话 67 | */ 68 | protected void endCall() { 69 | CallManager.getInstance().endCall(); 70 | onFinish(); 71 | } 72 | 73 | /** 74 | * 拒绝通话 75 | */ 76 | protected void rejectCall() { 77 | CallManager.getInstance().rejectCall(); 78 | onFinish(); 79 | } 80 | 81 | /** 82 | * 接听通话 83 | */ 84 | protected void answerCall() { 85 | CallManager.getInstance().answerCall(); 86 | } 87 | 88 | /** 89 | * 调用系统振动,触发按钮的震动反馈 90 | */ 91 | protected void vibrate() { 92 | vibrator.vibrate(88); 93 | } 94 | 95 | /** 96 | * 销毁界面时做一些自己的操作 97 | */ 98 | @Override public void onFinish() { 99 | super.onFinish(); 100 | } 101 | 102 | @Override protected void onStart() { 103 | super.onStart(); 104 | EventBus.getDefault().register(this); 105 | } 106 | 107 | @Override protected void onStop() { 108 | super.onStop(); 109 | EventBus.getDefault().unregister(this); 110 | } 111 | 112 | @Override protected void onResume() { 113 | // 判断当前通话状态,如果已经挂断,则关闭通话界面 114 | if (CallManager.getInstance().getCallState() == CallManager.CallState.DISCONNECTED) { 115 | onFinish(); 116 | return; 117 | } else { 118 | CallManager.getInstance().removeFloatWindow(); 119 | } 120 | super.onResume(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/CallEvent.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import com.hyphenate.chat.EMCallStateChangeListener; 4 | 5 | /** 6 | * 通话相关事件传递对象 7 | * Created by lzan13 on 2017/3/24. 8 | */ 9 | public class CallEvent { 10 | 11 | private boolean isState; 12 | private boolean isTime; 13 | private EMCallStateChangeListener.CallState callState; 14 | private EMCallStateChangeListener.CallError callError; 15 | 16 | public EMCallStateChangeListener.CallState getCallState() { 17 | return callState; 18 | } 19 | 20 | public void setCallState(EMCallStateChangeListener.CallState callState) { 21 | this.callState = callState; 22 | } 23 | 24 | public EMCallStateChangeListener.CallError getCallError() { 25 | return callError; 26 | } 27 | 28 | public void setCallError(EMCallStateChangeListener.CallError callError) { 29 | this.callError = callError; 30 | } 31 | 32 | public boolean isState() { 33 | return isState; 34 | } 35 | 36 | public void setState(boolean state) { 37 | isState = state; 38 | } 39 | 40 | public boolean isTime() { 41 | return isTime; 42 | } 43 | 44 | public void setTime(boolean time) { 45 | isTime = time; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/CallManager.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationManager; 5 | import android.app.PendingIntent; 6 | import android.bluetooth.BluetoothAdapter; 7 | import android.bluetooth.BluetoothHeadset; 8 | import android.bluetooth.BluetoothProfile; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.media.AudioAttributes; 12 | import android.media.AudioManager; 13 | import android.media.SoundPool; 14 | import android.support.v4.app.NotificationCompat; 15 | 16 | import com.hyphenate.chat.EMCallManager; 17 | import com.hyphenate.chat.EMClient; 18 | import com.hyphenate.chat.EMMessage; 19 | import com.hyphenate.chat.EMTextMessageBody; 20 | import com.hyphenate.exceptions.EMNoActiveCallException; 21 | import com.hyphenate.exceptions.EMServiceNotReadyException; 22 | 23 | import com.vmloft.develop.app.demo.call.R; 24 | import com.vmloft.develop.library.tools.utils.VMLog; 25 | 26 | import java.util.Timer; 27 | import java.util.TimerTask; 28 | 29 | import org.greenrobot.eventbus.EventBus; 30 | 31 | /** 32 | * Created by lzan13 on 2017/2/8. 33 | * 34 | * 实时音视频通话管理类,这是一个单例类,用来管理 app 通话操作 35 | */ 36 | public class CallManager { 37 | 38 | // 上下文菜单 39 | private Context context; 40 | 41 | // 蓝牙相关对象 42 | private BluetoothAdapter bluetoothAdapter; 43 | private BluetoothHeadset bluetoothHeadset; 44 | 45 | // 单例类实例 46 | private static CallManager instance; 47 | 48 | // 通知栏提醒管理类 49 | private NotificationManager notificationManager; 50 | private int callNotificationId = 0526; 51 | 52 | // 音频管理器 53 | private AudioManager audioManager; 54 | // 音频池 55 | private SoundPool soundPool; 56 | // 声音资源 id 57 | private int streamID; 58 | private int loadId; 59 | private boolean isLoaded = false; 60 | 61 | // 通话状态监听 62 | private CallStateListener callStateListener; 63 | 64 | // 记录通话方向,是呼出还是呼入 65 | private boolean isInComingCall = true; 66 | private boolean isExternalInputData = false; 67 | // 设备相关开关 68 | private boolean isOpenCamera = true; 69 | private boolean isOpenMic = true; 70 | private boolean isOpenSpeaker = true; 71 | private boolean isOpenRecord = false; 72 | 73 | // 计时器 74 | private Timer timer; 75 | // 通话时间 76 | private int callTime = 0; 77 | 78 | // 当前通话对象 id 79 | private String chatId; 80 | private CallState callState = CallState.DISCONNECTED; 81 | private CallType callType = CallType.VIDEO; 82 | private EndType endType = EndType.CANCEL; 83 | 84 | /** 85 | * 私有化构造函数 86 | */ 87 | private CallManager() { 88 | } 89 | 90 | /** 91 | * 获取单例对象实例方法 92 | */ 93 | public static CallManager getInstance() { 94 | if (instance == null) { 95 | instance = new CallManager(); 96 | } 97 | return instance; 98 | } 99 | 100 | /** 101 | * 通话管理类的初始化 102 | */ 103 | public void init(Context context) { 104 | this.context = context; 105 | // 初始化蓝牙监听 106 | initBluetoothListener(); 107 | // 初始化音频池 108 | initSoundPool(); 109 | // 音频管理器 110 | audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 111 | 112 | /** 113 | * SDK 3.2.x 版本后通话相关设置,一定要在初始化后,开始音视频功能前设置,否则设置无效 114 | */ 115 | // 设置通话过程中对方如果离线是否发送离线推送通知,默认 false,这里需要和推送配合使用 116 | EMClient.getInstance().callManager().getCallOptions().setIsSendPushIfOffline(false); 117 | /** 118 | * 设置是否启用外部输入视频数据,默认 false,如果设置为 true,需要自己调用 119 | * {@link EMCallManager#inputExternalVideoData(byte[], int, int, int)}输入视频数据 120 | * 视频数据的格式是摄像头采集的格式即:NV21 420sp 自己手动传入时需要将 rgb 格式的数据转为 yuv 121 | */ 122 | EMClient.getInstance().callManager().getCallOptions().setEnableExternalVideoData(isExternalInputData); 123 | // 设置视频旋转角度,启动前和视频通话中均可设置 124 | //EMClient.getInstance().callManager().getCallOptions().setRotation(90); 125 | // 设置自动调节分辨率,默认为 true 126 | EMClient.getInstance().callManager().getCallOptions().enableFixedVideoResolution(true); 127 | /** 128 | * 设置视频通话最大和最小比特率,可以不用设置,比特率会根据分辨率进行计算,默认最大(800), 默认最小(80) 129 | * 这里的带宽是指理想带宽,指单人单线情况下的最低要求 130 | * >240p: 100k ~ 400kbps 131 | * >480p: 300k ~ 1Mbps 132 | * >720p: 900k ~ 2.5Mbps 133 | * >1080p: 2M ~ 5Mbps 134 | */ 135 | EMClient.getInstance().callManager().getCallOptions().setMaxVideoKbps(800); 136 | EMClient.getInstance().callManager().getCallOptions().setMinVideoKbps(80); 137 | // 需要录制视频 138 | EMClient.getInstance().callManager().getCallOptions().enableFixedVideoResolution(true); 139 | // 设置视频通话分辨率 默认是(640, 480) 140 | EMClient.getInstance().callManager().getCallOptions().setVideoResolution(640, 480); 141 | // 设置通话最大帧率,SDK 最大支持(30),默认(20) 142 | EMClient.getInstance().callManager().getCallOptions().setMaxVideoFrameRate(20); 143 | // 设置音视频通话采样率,一般不需要设置,为了减少噪音,可以讲采集了适当调低,这里默认设置32k 144 | EMClient.getInstance().callManager().getCallOptions().setAudioSampleRate(16000); 145 | EMClient.getInstance().callManager().getCallOptions().setMaxAudioKbps(16); 146 | // 设置录制视频采用 mov 编码 TODO 后期这个而接口需要移动到 EMCallOptions 中 147 | EMClient.getInstance().callManager().getVideoCallHelper().setPreferMovFormatEnable(true); 148 | // 设置通话音频源类型 149 | // EMClient.getInstance().callManager().getCallOptions().setCallAudioSource(MediaRecorder.AudioSource.MIC); 150 | } 151 | 152 | /** 153 | * 通话结束,保存一条记录通话的消息 154 | */ 155 | public void saveCallMessage() { 156 | VMLog.d("The call ends and the call log message is saved! " + endType); 157 | EMMessage message = null; 158 | EMTextMessageBody body = null; 159 | String content = null; 160 | if (isInComingCall) { 161 | message = EMMessage.createReceiveMessage(EMMessage.Type.TXT); 162 | message.setFrom(chatId); 163 | } else { 164 | message = EMMessage.createSendMessage(EMMessage.Type.TXT); 165 | message.setTo(chatId); 166 | } 167 | switch (endType) { 168 | case NORMAL: // 正常结束通话 169 | content = String.valueOf(getCallTime()); 170 | break; 171 | case CANCEL: // 取消 172 | content = context.getString(R.string.call_cancel); 173 | break; 174 | case CANCELLED: // 被取消 175 | content = context.getString(R.string.call_cancel_is_incoming); 176 | break; 177 | case BUSY: // 对方忙碌 178 | content = context.getString(R.string.call_busy); 179 | break; 180 | case OFFLINE: // 对方不在线 181 | content = context.getString(R.string.call_offline); 182 | break; 183 | case REJECT: // 拒绝的 184 | content = context.getString(R.string.call_reject_is_incoming); 185 | break; 186 | case REJECTED: // 被拒绝的 187 | content = context.getString(R.string.call_reject); 188 | break; 189 | case NORESPONSE: // 未响应 190 | content = context.getString(R.string.call_no_response); 191 | break; 192 | case TRANSPORT: // 建立连接失败 193 | content = context.getString(R.string.call_connection_fail); 194 | break; 195 | case DIFFERENT: // 通讯协议不同 196 | content = context.getString(R.string.call_offline); 197 | break; 198 | default: 199 | // 默认取消 200 | content = context.getString(R.string.call_cancel); 201 | break; 202 | } 203 | body = new EMTextMessageBody(content); 204 | message.addBody(body); 205 | message.setStatus(EMMessage.Status.SUCCESS); 206 | if (callType == CallType.VIDEO) { 207 | message.setAttribute("attr_call_video", true); 208 | } else { 209 | message.setAttribute("attr_call_voice", true); 210 | } 211 | message.setUnread(false); 212 | // 调用sdk的保存消息方法 213 | EMClient.getInstance().chatManager().saveMessage(message); 214 | } 215 | 216 | /** 217 | * 开始呼叫对方 218 | */ 219 | public void makeCall() { 220 | try { 221 | if (callType == CallType.VIDEO) { 222 | EMClient.getInstance() 223 | .callManager() 224 | .makeVideoCall(chatId, "{'ext':{'type':'video','key':'value'}}"); 225 | } else { 226 | EMClient.getInstance() 227 | .callManager() 228 | .makeVoiceCall(chatId, "{'ext':{'type':'voice','key':'value'}}"); 229 | } 230 | setEndType(EndType.CANCEL); 231 | } catch (EMServiceNotReadyException e) { 232 | e.printStackTrace(); 233 | } 234 | } 235 | 236 | /** 237 | * 拒绝通话 238 | */ 239 | public void rejectCall() { 240 | try { 241 | VMLog.i("rejectCall"); 242 | // 调用 SDK 的拒绝通话方法 243 | EMClient.getInstance().callManager().rejectCall(); 244 | // 设置结束原因为拒绝 245 | setEndType(EndType.REJECT); 246 | } catch (EMNoActiveCallException e) { 247 | e.printStackTrace(); 248 | } 249 | // 保存一条通话消息 250 | saveCallMessage(); 251 | // 通话结束,重置通话状态 252 | reset(); 253 | } 254 | 255 | /** 256 | * 结束通话 257 | */ 258 | public void endCall() { 259 | try { 260 | VMLog.i("endCall"); 261 | // 调用 SDK 的结束通话方法 262 | EMClient.getInstance().callManager().endCall(); 263 | } catch (EMNoActiveCallException e) { 264 | e.printStackTrace(); 265 | VMLog.e("结束通话失败:error %d - %s", e.getErrorCode(), e.getMessage()); 266 | } 267 | // 挂断电话调用保存消息方法 268 | saveCallMessage(); 269 | // 通话结束,重置通话状态 270 | reset(); 271 | } 272 | 273 | /** 274 | * 接听通话 275 | */ 276 | public boolean answerCall() { 277 | // 接听通话后关闭通知铃音 278 | stopCallSound(); 279 | // 调用接通通话方法 280 | try { 281 | VMLog.i("answerCall"); 282 | EMClient.getInstance().callManager().answerCall(); 283 | return true; 284 | } catch (EMNoActiveCallException e) { 285 | e.printStackTrace(); 286 | return false; 287 | } 288 | } 289 | 290 | /** 291 | * 打开扬声器 292 | * 主要是通过扬声器的开关以及设置音频播放模式来实现 293 | * 1、MODE_NORMAL:是正常模式,一般用于外放音频 294 | * 2、MODE_IN_CALL: 295 | * 3、MODE_IN_COMMUNICATION:这个和 CALL 都表示通讯模式,不过 CALL 在华为上不好使,故使用 COMMUNICATION 296 | * 4、MODE_RINGTONE:铃声模式 297 | */ 298 | public void openSpeaker() { 299 | // 检查是否已经开启扬声器 300 | if (!audioManager.isSpeakerphoneOn()) { 301 | // 打开扬声器 302 | audioManager.setSpeakerphoneOn(true); 303 | } 304 | if (callState == CallManager.CallState.ACCEPTED) { 305 | // 开启了扬声器之后,因为是进行通话,声音的模式也要设置成通讯模式 306 | audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 307 | } else { 308 | // 在播放通话音效时声音模式需要设置为铃音模式 309 | audioManager.setMode(AudioManager.MODE_NORMAL); 310 | } 311 | setOpenSpeaker(true); 312 | 313 | disconnectBluetoothAudio(); 314 | } 315 | 316 | /** 317 | * 关闭扬声器,即开启听筒播放模式 318 | * 更多内容看{@link #openSpeaker()} 319 | */ 320 | public void closeSpeaker() { 321 | // 检查是否已经开启扬声器 322 | if (audioManager.isSpeakerphoneOn()) { 323 | // 关闭扬声器 324 | audioManager.setSpeakerphoneOn(false); 325 | } 326 | if (callState == CallManager.CallState.ACCEPTED) { 327 | // 设置声音模式为通讯模式 328 | audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); 329 | } else { 330 | // 在播放通话音效时声音模式需要设置为铃音模式 331 | audioManager.setMode(AudioManager.MODE_NORMAL); 332 | } 333 | setOpenSpeaker(false); 334 | 335 | connectBluetoothAudio(); 336 | } 337 | 338 | /** 339 | * 初始化蓝牙监听 340 | */ 341 | private void initBluetoothListener() { 342 | bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 343 | if (bluetoothAdapter != null) { 344 | bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() { 345 | @Override 346 | public void onServiceConnected(int profile, BluetoothProfile proxy) { 347 | bluetoothHeadset = (BluetoothHeadset) proxy; 348 | VMLog.d("bluetooth is "); 349 | } 350 | 351 | @Override 352 | public void onServiceDisconnected(int profile) { 353 | bluetoothHeadset = null; 354 | } 355 | }, BluetoothProfile.HEADSET); 356 | } 357 | } 358 | 359 | /** 360 | * 连接蓝牙音频输出设备,通过蓝牙输出声音 361 | */ 362 | private void connectBluetoothAudio() { 363 | try { 364 | if (bluetoothHeadset != null) { 365 | bluetoothHeadset.startVoiceRecognition(bluetoothHeadset.getConnectedDevices() 366 | .get(0)); 367 | } 368 | } catch (Exception e) { 369 | e.printStackTrace(); 370 | } 371 | } 372 | 373 | /** 374 | * 与蓝牙输出设备断开连接 375 | */ 376 | private void disconnectBluetoothAudio() { 377 | try { 378 | if (bluetoothHeadset != null) { 379 | bluetoothHeadset.stopVoiceRecognition(bluetoothHeadset.getConnectedDevices() 380 | .get(0)); 381 | } 382 | } catch (Exception e) { 383 | e.printStackTrace(); 384 | } 385 | } 386 | 387 | /** 388 | * ----------------------------- Sound start ----------------------------- 389 | * 初始化 SoundPool 390 | */ 391 | private void initSoundPool() { 392 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { 393 | AudioAttributes attributes = new AudioAttributes.Builder() 394 | // 设置音频要用在什么地方,这里选择电话通知铃音 395 | .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) 396 | .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 397 | .build(); 398 | // 当系统的 SDK 版本高于21时,使用 build 的方式实例化 SoundPool 399 | soundPool = new SoundPool.Builder().setAudioAttributes(attributes) 400 | .setMaxStreams(1) 401 | .build(); 402 | } else { 403 | // 老版本使用构造函数方式实例化 SoundPool,MODE 设置为铃音 MODE_RINGTONE 404 | soundPool = new SoundPool(1, AudioManager.MODE_RINGTONE, 0); 405 | } 406 | } 407 | 408 | /** 409 | * 加载音效资源 410 | */ 411 | private void loadSound() { 412 | if (isInComingCall) { 413 | loadId = soundPool.load(context, R.raw.sound_call_incoming, 1); 414 | } else { 415 | loadId = soundPool.load(context, R.raw.sound_calling, 1); 416 | } 417 | } 418 | 419 | /** 420 | * 尝试播放呼叫通话提示音 421 | */ 422 | public void attemptPlayCallSound() { 423 | // 检查音频资源是否已经加载完毕 424 | if (isLoaded) { 425 | playCallSound(); 426 | } else { 427 | // 播放之前先去加载音效 428 | loadSound(); 429 | // 设置资源加载监听,也因为加载资源在单独的进程,需要时间,所以等监听到加载完成才能播放 430 | soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() { 431 | @Override 432 | public void onLoadComplete(SoundPool soundPool, int i, int i1) { 433 | VMLog.d("SoundPool load complete! loadId: %d", loadId); 434 | isLoaded = true; 435 | // 首次监听到加载完毕,开始播放音频 436 | playCallSound(); 437 | } 438 | }); 439 | } 440 | } 441 | 442 | /** 443 | * 播放音频 444 | */ 445 | private void playCallSound() { 446 | // 打开扬声器 447 | openSpeaker(); 448 | // 设置音频管理器音频模式为铃音模式 449 | audioManager.setMode(AudioManager.MODE_NORMAL); 450 | // 播放提示音,返回一个播放的音频id,等下停止播放需要用到 451 | if (soundPool != null) { 452 | streamID = soundPool.play(loadId, // 播放资源id;就是加载到SoundPool里的音频资源顺序 453 | 0.5f, // 左声道音量 454 | 0.5f, // 右声道音量 455 | 1, // 优先级,数值越高,优先级越大 456 | -1, // 是否循环;0 不循环,-1 循环,N 表示循环次数 457 | 1); // 播放速率;从0.5-2,一般设置为1,表示正常播放 458 | } 459 | } 460 | 461 | /** 462 | * 关闭音效的播放,并释放资源 463 | */ 464 | protected void stopCallSound() { 465 | if (soundPool != null) { 466 | // 停止播放音效 467 | soundPool.stop(streamID); 468 | // 卸载音效 469 | //soundPool.unload(loadId); 470 | // 释放资源 471 | //soundPool.release(); 472 | } 473 | } 474 | 475 | /** 476 | * ----------------------------- Call state ----------------------------- 477 | * 注册通话状态监听,监听音视频通话状态 478 | * 状态监听详细实现在 {@link CallStateListener} 类中 479 | */ 480 | public void registerCallStateListener() { 481 | if (callStateListener == null) { 482 | callStateListener = new CallStateListener(); 483 | } 484 | EMClient.getInstance().callManager().addCallStateChangeListener(callStateListener); 485 | } 486 | 487 | /** 488 | * 删除通话状态监听 489 | */ 490 | private void unregisterCallStateListener() { 491 | if (callStateListener != null) { 492 | EMClient.getInstance().callManager().removeCallStateChangeListener(callStateListener); 493 | callStateListener = null; 494 | } 495 | } 496 | 497 | /** 498 | * 添加通话悬浮窗并发送通知栏提醒 499 | */ 500 | public void addFloatWindow() { 501 | // 发送通知栏提醒 502 | addCallNotification(); 503 | // 开启悬浮窗 504 | FloatWindow.getInstance(context).addFloatWindow(); 505 | } 506 | 507 | /** 508 | * 移除通话悬浮窗和通知栏提醒 509 | */ 510 | public void removeFloatWindow() { 511 | // 取消通知栏提醒 512 | cancelCallNotification(); 513 | // 关闭悬浮窗 514 | FloatWindow.getInstance(context).removeFloatWindow(); 515 | } 516 | 517 | /** 518 | * 发送通知栏提醒,告知用户通话继续进行中 519 | */ 520 | private void addCallNotification() { 521 | notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 522 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context); 523 | 524 | builder.setSmallIcon(R.mipmap.ic_launcher); 525 | builder.setPriority(Notification.PRIORITY_HIGH); 526 | builder.setAutoCancel(true); 527 | builder.setDefaults(Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS); 528 | 529 | builder.setContentText("通话进行中,点击恢复"); 530 | 531 | builder.setContentTitle(context.getString(R.string.app_name)); 532 | Intent intent = new Intent(); 533 | if (callType == CallType.VIDEO) { 534 | intent.setClass(context, VideoCallActivity.class); 535 | } else { 536 | intent.setClass(context, VoiceCallActivity.class); 537 | } 538 | PendingIntent pIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); 539 | builder.setContentIntent(pIntent); 540 | builder.setOngoing(true); 541 | 542 | builder.setWhen(System.currentTimeMillis()); 543 | 544 | notificationManager.notify(callNotificationId, builder.build()); 545 | } 546 | 547 | /** 548 | * 取消通话状态通知栏提醒 549 | */ 550 | public void cancelCallNotification() { 551 | if (notificationManager != null) { 552 | notificationManager.cancel(callNotificationId); 553 | } 554 | } 555 | 556 | /** 557 | * 开始通话计时,这里在全局管理器中开启一个定时器进行计时,可以做到最小化,以及后台时进行计时 558 | */ 559 | public void startCallTime() { 560 | final CallEvent event = new CallEvent(); 561 | EventBus.getDefault().post(event); 562 | event.setTime(true); 563 | if (timer == null) { 564 | timer = new Timer(); 565 | } 566 | timer.purge(); 567 | TimerTask task = new TimerTask() { 568 | @Override 569 | public void run() { 570 | callTime++; 571 | EventBus.getDefault().post(event); 572 | } 573 | }; 574 | timer.scheduleAtFixedRate(task, 1000, 1000); 575 | } 576 | 577 | /** 578 | * 停止计时 579 | */ 580 | public void stopCallTime() { 581 | if (timer != null) { 582 | timer.purge(); 583 | timer.cancel(); 584 | timer = null; 585 | } 586 | callTime = 0; 587 | } 588 | 589 | /** 590 | * 释放资源 591 | */ 592 | public void reset() { 593 | isOpenCamera = true; 594 | isOpenMic = true; 595 | isOpenSpeaker = true; 596 | isOpenRecord = false; 597 | // 设置通话状态为已断开 598 | setCallState(CallState.DISCONNECTED); 599 | // 停止计时 600 | stopCallTime(); 601 | // 取消注册通话状态的监听 602 | unregisterCallStateListener(); 603 | // 释放音频资源 604 | if (soundPool != null) { 605 | // 停止播放音效 606 | soundPool.stop(streamID); 607 | } 608 | // 重置音频管理器 609 | if (audioManager != null) { 610 | audioManager.setSpeakerphoneOn(true); 611 | audioManager.setMode(AudioManager.MODE_NORMAL); 612 | } 613 | } 614 | 615 | /** 616 | * 相关的 get 以及 set 方法 617 | */ 618 | public CallState getCallState() { 619 | return callState; 620 | } 621 | 622 | public void setCallState(CallState callState) { 623 | this.callState = callState; 624 | } 625 | 626 | public CallType getCallType() { 627 | return callType; 628 | } 629 | 630 | public void setCallType(CallType callType) { 631 | this.callType = callType; 632 | } 633 | 634 | public String getChatId() { 635 | return chatId; 636 | } 637 | 638 | public void setChatId(String chatId) { 639 | this.chatId = chatId; 640 | } 641 | 642 | public boolean isInComingCall() { 643 | return isInComingCall; 644 | } 645 | 646 | public void setInComingCall(boolean isInComingCall) { 647 | this.isInComingCall = isInComingCall; 648 | } 649 | 650 | public int getCallTime() { 651 | return callTime; 652 | } 653 | 654 | public void setEndType(EndType endType) { 655 | this.endType = endType; 656 | } 657 | 658 | public EndType getEndType() { 659 | return endType; 660 | } 661 | 662 | public boolean isExternalInputData() { 663 | return isExternalInputData; 664 | } 665 | 666 | public void setExternalInputData(boolean externalInputData) { 667 | isExternalInputData = externalInputData; 668 | } 669 | 670 | public boolean isOpenCamera() { 671 | return isOpenCamera; 672 | } 673 | 674 | public void setOpenCamera(boolean openCamera) { 675 | isOpenCamera = openCamera; 676 | } 677 | 678 | public boolean isOpenMic() { 679 | return isOpenMic; 680 | } 681 | 682 | public void setOpenMic(boolean openMic) { 683 | isOpenMic = openMic; 684 | } 685 | 686 | public boolean isOpenSpeaker() { 687 | return isOpenSpeaker; 688 | } 689 | 690 | public void setOpenSpeaker(boolean openSpeaker) { 691 | isOpenSpeaker = openSpeaker; 692 | } 693 | 694 | public boolean isOpenRecord() { 695 | return isOpenRecord; 696 | } 697 | 698 | public void setOpenRecord(boolean openRecord) { 699 | isOpenRecord = openRecord; 700 | } 701 | 702 | /** 703 | * 通话类型 704 | */ 705 | public enum CallType { 706 | VIDEO, 707 | // 视频通话 708 | VOICE // 音频通话 709 | } 710 | 711 | /** 712 | * 通话状态枚举值 713 | */ 714 | public enum CallState { 715 | CONNECTING, 716 | // 连接中 717 | CONNECTED, 718 | // 连接成功,等待接受 719 | ACCEPTED, 720 | // 通话中 721 | DISCONNECTED // 通话中断 722 | 723 | } 724 | 725 | /** 726 | * 通话结束状态类型 727 | */ 728 | public enum EndType { 729 | NORMAL, 730 | // 正常结束通话 731 | CANCEL, 732 | // 取消 733 | CANCELLED, 734 | // 被取消 735 | BUSY, 736 | // 对方忙碌 737 | OFFLINE, 738 | // 对方不在线 739 | REJECT, 740 | // 拒绝的 741 | REJECTED, 742 | // 被拒绝的 743 | NORESPONSE, 744 | // 未响应 745 | TRANSPORT, 746 | // 建立连接失败 747 | DIFFERENT // 通讯协议不同 748 | } 749 | } 750 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/CallPushProvider.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import com.hyphenate.EMCallBack; 4 | import com.hyphenate.chat.EMCallManager; 5 | import com.hyphenate.chat.EMClient; 6 | import com.hyphenate.chat.EMMessage; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | /** 11 | * Created by lzan13 on 2017/5/5. 12 | * 通话推送信息回掉接口,主要是用来实现当对方不在线时,发送一条消息,推送给对方,让对方上线后能继续收到呼叫 13 | */ 14 | public class CallPushProvider implements EMCallManager.EMCallPushProvider { 15 | @Override public void onRemoteOffline(String username) { 16 | EMMessage message = EMMessage.createTxtSendMessage("有人呼叫你,开启 APP 接听吧", username); 17 | if (CallManager.getInstance().getCallType() == CallManager.CallType.VIDEO) { 18 | message.setAttribute("attr_call_video", true); 19 | } else { 20 | message.setAttribute("attr_call_voice", true); 21 | } 22 | // 设置强制推送 23 | message.setAttribute("em_force_notification", "true"); 24 | // 设置自定义推送提示 25 | JSONObject extObj = new JSONObject(); 26 | try { 27 | extObj.put("em_push_title", "有人呼叫你,开启 APP 接听吧"); 28 | extObj.put("extern", "定义推送扩展内容"); 29 | } catch (JSONException e) { 30 | e.printStackTrace(); 31 | } 32 | message.setAttribute("em_apns_ext", extObj); 33 | message.setMessageStatusCallback(new EMCallBack() { 34 | @Override public void onSuccess() { 35 | 36 | } 37 | 38 | @Override public void onError(int i, String s) { 39 | 40 | } 41 | 42 | @Override public void onProgress(int i, String s) { 43 | 44 | } 45 | }); 46 | EMClient.getInstance().chatManager().sendMessage(message); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/CallReceiver.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import com.hyphenate.chat.EMClient; 7 | import com.vmloft.develop.library.tools.utils.VMLog; 8 | 9 | /** 10 | * Created by lzan13 on 2016/10/18. 11 | * 12 | * 通话呼叫监听广播实现,用来监听其他账户对自己的呼叫 13 | */ 14 | public class CallReceiver extends BroadcastReceiver { 15 | 16 | public CallReceiver() { 17 | } 18 | 19 | @Override public void onReceive(Context context, Intent intent) { 20 | // 判断环信是否登录成功 21 | if (!EMClient.getInstance().isLoggedInBefore()) { 22 | return; 23 | } 24 | 25 | // 呼叫方的usernmae 26 | String callFrom = intent.getStringExtra("from"); 27 | // 呼叫类型,有语音和视频两种 28 | String callType = intent.getStringExtra("type"); 29 | // 呼叫接收方, 30 | String callTo = intent.getStringExtra("to"); 31 | // 获取通话时的扩展字段 32 | String callExt = EMClient.getInstance().callManager().getCurrentCallSession().getExt(); 33 | VMLog.d("call extension data %s", callExt); 34 | Intent callIntent = new Intent(); 35 | // 根据通话类型跳转到语音通话或视频通话界面 36 | if (callType.equals("video")) { 37 | // 设置当前通话类型为视频通话 38 | CallManager.getInstance().setCallType(CallManager.CallType.VIDEO); 39 | callIntent.setClass(context, VideoCallActivity.class); 40 | } else if (callType.equals("voice")) { 41 | // 设置当前通话类型为语音通话 42 | CallManager.getInstance().setCallType(CallManager.CallType.VOICE); 43 | callIntent.setClass(context, VoiceCallActivity.class); 44 | } 45 | // 初始化通化管理类的一些属性 46 | CallManager.getInstance().setChatId(callFrom); 47 | CallManager.getInstance().setInComingCall(true); 48 | 49 | // 设置 activity 启动方式 50 | callIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 51 | context.startActivity(callIntent); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/CallStateListener.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import com.hyphenate.chat.EMCallStateChangeListener; 4 | import com.vmloft.develop.library.tools.utils.VMLog; 5 | import org.greenrobot.eventbus.EventBus; 6 | 7 | /** 8 | * Created by lzan13 on 2016/10/18. 9 | * 10 | * 通话状态监听类,用来监听通话过程中状态的变化 11 | */ 12 | 13 | public class CallStateListener implements EMCallStateChangeListener { 14 | 15 | @Override public void onCallStateChanged(CallState callState, CallError callError) { 16 | CallEvent event = new CallEvent(); 17 | event.setState(true); 18 | event.setCallError(callError); 19 | event.setCallState(callState); 20 | EventBus.getDefault().post(event); 21 | switch (callState) { 22 | case CONNECTING: // 正在呼叫对方,TODO 没见回调过 23 | VMLog.i("正在呼叫对方" + callError); 24 | CallManager.getInstance().setCallState(CallManager.CallState.CONNECTING); 25 | break; 26 | case CONNECTED: // 正在等待对方接受呼叫申请(对方申请与你进行通话) 27 | VMLog.i("正在连接" + callError); 28 | CallManager.getInstance().setCallState(CallManager.CallState.CONNECTED); 29 | break; 30 | case ACCEPTED: // 通话已接通 31 | VMLog.i("通话已接通"); 32 | CallManager.getInstance().stopCallSound(); 33 | CallManager.getInstance().startCallTime(); 34 | CallManager.getInstance().setEndType(CallManager.EndType.NORMAL); 35 | CallManager.getInstance().setCallState(CallManager.CallState.ACCEPTED); 36 | break; 37 | case DISCONNECTED: // 通话已中断 38 | VMLog.i("通话已结束" + callError); 39 | // 通话结束,重置通话状态 40 | if (callError == CallError.ERROR_UNAVAILABLE) { 41 | VMLog.i("对方不在线" + callError); 42 | CallManager.getInstance().setEndType(CallManager.EndType.OFFLINE); 43 | } else if (callError == CallError.ERROR_BUSY) { 44 | VMLog.i("对方正忙" + callError); 45 | CallManager.getInstance().setEndType(CallManager.EndType.BUSY); 46 | } else if (callError == CallError.REJECTED) { 47 | VMLog.i("对方已拒绝" + callError); 48 | CallManager.getInstance().setEndType(CallManager.EndType.REJECTED); 49 | } else if (callError == CallError.ERROR_NORESPONSE) { 50 | VMLog.i("对方未响应,可能手机不在身边" + callError); 51 | CallManager.getInstance().setEndType(CallManager.EndType.NORESPONSE); 52 | } else if (callError == CallError.ERROR_TRANSPORT) { 53 | VMLog.i("连接建立失败" + callError); 54 | CallManager.getInstance().setEndType(CallManager.EndType.TRANSPORT); 55 | } else if (callError == CallError.ERROR_LOCAL_SDK_VERSION_OUTDATED) { 56 | VMLog.i("双方通讯协议不同" + callError); 57 | CallManager.getInstance().setEndType(CallManager.EndType.DIFFERENT); 58 | } else if (callError == CallError.ERROR_REMOTE_SDK_VERSION_OUTDATED) { 59 | VMLog.i("双方通讯协议不同" + callError); 60 | CallManager.getInstance().setEndType(CallManager.EndType.DIFFERENT); 61 | } else if (callError == CallError.ERROR_NO_DATA) { 62 | VMLog.i("没有通话数据" + callError); 63 | } else { 64 | VMLog.i("通话已结束 %s", callError); 65 | if (CallManager.getInstance().getEndType() == CallManager.EndType.CANCEL) { 66 | CallManager.getInstance().setEndType(CallManager.EndType.CANCELLED); 67 | } 68 | } 69 | // 通话结束,保存消息 70 | CallManager.getInstance().saveCallMessage(); 71 | CallManager.getInstance().reset(); 72 | break; 73 | case NETWORK_DISCONNECTED: 74 | VMLog.i("对方网络不可用"); 75 | break; 76 | case NETWORK_NORMAL: 77 | VMLog.i("网络正常"); 78 | break; 79 | case NETWORK_UNSTABLE: 80 | if (callError == EMCallStateChangeListener.CallError.ERROR_NO_DATA) { 81 | VMLog.i("没有通话数据" + callError); 82 | } else { 83 | VMLog.i("网络不稳定" + callError); 84 | } 85 | break; 86 | case VIDEO_PAUSE: 87 | VMLog.i("视频传输已暂停"); 88 | break; 89 | case VIDEO_RESUME: 90 | VMLog.i("视频传输已恢复"); 91 | break; 92 | case VOICE_PAUSE: 93 | VMLog.i("语音传输已暂停"); 94 | break; 95 | case VOICE_RESUME: 96 | VMLog.i("语音传输已恢复"); 97 | break; 98 | default: 99 | break; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/FloatWindow.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.graphics.PixelFormat; 6 | import android.view.Gravity; 7 | import android.view.LayoutInflater; 8 | import android.view.MotionEvent; 9 | import android.view.View; 10 | import android.view.WindowManager; 11 | import android.widget.RelativeLayout; 12 | import android.widget.TextView; 13 | 14 | import com.hyphenate.chat.EMCallStateChangeListener; 15 | import com.hyphenate.chat.EMClient; 16 | import com.hyphenate.media.EMCallSurfaceView; 17 | import com.superrtc.sdk.VideoView; 18 | import com.vmloft.develop.app.demo.call.R; 19 | import com.vmloft.develop.library.tools.utils.VMDimen; 20 | import com.vmloft.develop.library.tools.utils.VMLog; 21 | 22 | import org.greenrobot.eventbus.EventBus; 23 | import org.greenrobot.eventbus.Subscribe; 24 | import org.greenrobot.eventbus.ThreadMode; 25 | 26 | /** 27 | * Created by lzan13 on 2017/3/27. 28 | * 29 | * 音视频通话悬浮窗操作类 30 | */ 31 | public class FloatWindow { 32 | 33 | // 上下文菜单 34 | private Context context; 35 | 36 | // 当前单例类实例 37 | private static FloatWindow instance; 38 | 39 | private WindowManager windowManager = null; 40 | private WindowManager.LayoutParams layoutParams = null; 41 | 42 | // 悬浮窗需要显示的布局 43 | private View floatView; 44 | private TextView callTimeView; 45 | 46 | private EMCallSurfaceView localView; 47 | private EMCallSurfaceView oppositeView; 48 | 49 | public FloatWindow(Context context) { 50 | this.context = context; 51 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 52 | } 53 | 54 | public static FloatWindow getInstance(Context context) { 55 | if (instance == null) { 56 | instance = new FloatWindow(context); 57 | } 58 | return instance; 59 | } 60 | 61 | /** 62 | * 开始展示悬浮窗 63 | */ 64 | public void addFloatWindow() { 65 | if (floatView != null) { 66 | return; 67 | } 68 | EventBus.getDefault() 69 | .register(this); 70 | layoutParams = new WindowManager.LayoutParams(); 71 | // 位置为右侧顶部 72 | layoutParams.gravity = Gravity.LEFT | Gravity.TOP; 73 | // 设置宽高自适应 74 | layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; 75 | layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; 76 | 77 | // 设置悬浮窗透明 78 | layoutParams.format = PixelFormat.TRANSPARENT; 79 | 80 | // 设置窗口类型 81 | layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; 82 | 83 | // 设置窗口标志类型,其中 FLAG_NOT_FOCUSABLE 是放置当前悬浮窗拦截点击事件,造成桌面控件不可操作 84 | layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; 85 | 86 | // 获取要现实的布局 87 | floatView = LayoutInflater.from(context) 88 | .inflate(R.layout.widget_float_window, null); 89 | // 添加悬浮窗 View 到窗口 90 | windowManager.addView(floatView, layoutParams); 91 | if (CallManager.getInstance() 92 | .getCallType() == CallManager.CallType.VOICE) { 93 | floatView.findViewById(R.id.layout_call_voice) 94 | .setVisibility(View.VISIBLE); 95 | floatView.findViewById(R.id.layout_call_video) 96 | .setVisibility(View.GONE); 97 | callTimeView = (TextView) floatView.findViewById(R.id.text_call_time); 98 | refreshCallTime(); 99 | } else { 100 | setupSurfaceView(); 101 | } 102 | 103 | // 当点击悬浮窗时,返回到通话界面 104 | floatView.setOnClickListener(new View.OnClickListener() { 105 | @Override 106 | public void onClick(View v) { 107 | Intent intent = new Intent(); 108 | if (CallManager.getInstance() 109 | .getCallType() == CallManager.CallType.VOICE) { 110 | intent.setClass(context, VoiceCallActivity.class); 111 | } else { 112 | intent.setClass(context, VideoCallActivity.class); 113 | } 114 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 115 | context.startActivity(intent); 116 | } 117 | }); 118 | 119 | //设置监听浮动窗口的触摸移动 120 | floatView.setOnTouchListener(new View.OnTouchListener() { 121 | boolean result = false; 122 | 123 | float x = 0; 124 | float y = 0; 125 | float startX = 0; 126 | float startY = 0; 127 | 128 | @Override 129 | public boolean onTouch(View v, MotionEvent event) { 130 | switch (event.getAction()) { 131 | case MotionEvent.ACTION_DOWN: 132 | result = false; 133 | x = event.getX(); 134 | y = event.getY(); 135 | startX = event.getRawX(); 136 | startY = event.getRawY(); 137 | VMLog.d("start x: %f, y: %f", startX, startY); 138 | break; 139 | case MotionEvent.ACTION_MOVE: 140 | VMLog.d("move x: %f, y: %f", event.getRawX(), event.getRawY()); 141 | // 当移动距离大于特定值时,表示是拖动悬浮窗,则不触发后边的点击监听 142 | if (Math.abs(event.getRawX() - startX) > 20 || Math.abs(event.getRawY() - startY) > 20) { 143 | result = true; 144 | } 145 | // getRawX 获取触摸点相对于屏幕的坐标,getX 相对于当前悬浮窗坐标 146 | // 根据当前触摸点 X 坐标计算悬浮窗 X 坐标, 147 | layoutParams.x = (int) (event.getRawX() - x); 148 | // 根据当前触摸点 Y 坐标计算悬浮窗 Y 坐标,减25为状态栏的高度 149 | layoutParams.y = (int) (event.getRawY() - y - 25); 150 | // 刷新悬浮窗 151 | windowManager.updateViewLayout(floatView, layoutParams); 152 | break; 153 | case MotionEvent.ACTION_UP: 154 | break; 155 | } 156 | return result; 157 | } 158 | }); 159 | } 160 | 161 | /** 162 | * 设置本地与远程画面显示控件 163 | */ 164 | private void setupSurfaceView() { 165 | floatView.findViewById(R.id.layout_call_voice) 166 | .setVisibility(View.GONE); 167 | floatView.findViewById(R.id.layout_call_video) 168 | .setVisibility(View.VISIBLE); 169 | 170 | RelativeLayout surfaceLayout = (RelativeLayout) floatView.findViewById(R.id.layout_call_video); 171 | 172 | // 将 SurfaceView设置给 SDK 173 | surfaceLayout.removeAllViews(); 174 | 175 | localView = new EMCallSurfaceView(context); 176 | oppositeView = new EMCallSurfaceView(context); 177 | 178 | int lw = VMDimen.dp2px(24); 179 | int lh = VMDimen.dp2px(32); 180 | int ow = VMDimen.dp2px(96); 181 | int oh = VMDimen.dp2px(128); 182 | RelativeLayout.LayoutParams localParams = new RelativeLayout.LayoutParams(lw, lh); 183 | RelativeLayout.LayoutParams oppositeParams = new RelativeLayout.LayoutParams(ow, oh); 184 | // 设置本地图像靠右 185 | localParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); 186 | 187 | // 设置本地预览图像显示在最上层 188 | localView.setZOrderOnTop(false); 189 | localView.setZOrderMediaOverlay(true); 190 | // 将 view 添加到界面 191 | surfaceLayout.addView(localView, localParams); 192 | surfaceLayout.addView(oppositeView, oppositeParams); 193 | 194 | // 设置通话界面画面填充方式 195 | localView.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFill); 196 | oppositeView.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFill); 197 | // 设置本地以及对方显示画面控件,这个要设置在上边几个方法之后,不然会概率出现接收方无画面 198 | EMClient.getInstance() 199 | .callManager() 200 | .setSurfaceView(localView, oppositeView); 201 | } 202 | 203 | /** 204 | * 停止悬浮窗 205 | */ 206 | public void removeFloatWindow() { 207 | EventBus.getDefault() 208 | .unregister(this); 209 | if (localView != null) { 210 | if (localView.getRenderer() != null) { 211 | localView.getRenderer() 212 | .dispose(); 213 | } 214 | localView.release(); 215 | localView = null; 216 | } 217 | if (oppositeView != null) { 218 | if (oppositeView.getRenderer() != null) { 219 | oppositeView.getRenderer() 220 | .dispose(); 221 | } 222 | oppositeView.release(); 223 | oppositeView = null; 224 | } 225 | if (windowManager != null && floatView != null) { 226 | windowManager.removeView(floatView); 227 | floatView = null; 228 | } 229 | } 230 | 231 | @Subscribe(threadMode = ThreadMode.MAIN) 232 | public void onEventBus(CallEvent event) { 233 | if (event.isState()) { 234 | refreshCallView(event); 235 | } 236 | if (event.isTime() && CallManager.getInstance() 237 | .getCallType() == CallManager.CallType.VOICE) { 238 | refreshCallTime(); 239 | } 240 | } 241 | 242 | /** 243 | * 刷新通话界面 244 | */ 245 | private void refreshCallView(CallEvent event) { 246 | EMCallStateChangeListener.CallError callError = event.getCallError(); 247 | EMCallStateChangeListener.CallState callState = event.getCallState(); 248 | switch (callState) { 249 | case CONNECTING: // 正在呼叫对方,TODO 没见回调过 250 | VMLog.i("正在呼叫对方" + callError); 251 | break; 252 | case CONNECTED: // 正在等待对方接受呼叫申请(对方申请与你进行通话) 253 | VMLog.i("正在连接" + callError); 254 | break; 255 | case ACCEPTED: // 通话已接通 256 | VMLog.i("通话已接通"); 257 | break; 258 | case DISCONNECTED: // 通话已中断 259 | VMLog.i("通话已结束" + callError); 260 | CallManager.getInstance() 261 | .removeFloatWindow(); 262 | break; 263 | // TODO 3.3.0版本 SDK 下边几个暂时都没有回调 264 | case NETWORK_UNSTABLE: 265 | if (callError == EMCallStateChangeListener.CallError.ERROR_NO_DATA) { 266 | VMLog.i("没有通话数据" + callError); 267 | } else { 268 | VMLog.i("网络不稳定" + callError); 269 | } 270 | break; 271 | case NETWORK_NORMAL: 272 | VMLog.i("网络正常"); 273 | break; 274 | case VIDEO_PAUSE: 275 | VMLog.i("视频传输已暂停"); 276 | break; 277 | case VIDEO_RESUME: 278 | VMLog.i("视频传输已恢复"); 279 | break; 280 | case VOICE_PAUSE: 281 | VMLog.i("语音传输已暂停"); 282 | break; 283 | case VOICE_RESUME: 284 | VMLog.i("语音传输已恢复"); 285 | break; 286 | default: 287 | break; 288 | } 289 | } 290 | 291 | private void refreshCallTime() { 292 | int t = CallManager.getInstance() 293 | .getCallTime(); 294 | int h = t / 60 / 60; 295 | int m = t / 60 % 60; 296 | int s = t % 60 % 60; 297 | String time = ""; 298 | if (h > 9) { 299 | time = "" + h; 300 | } else { 301 | time = "0" + h; 302 | } 303 | if (m > 9) { 304 | time += ":" + m; 305 | } else { 306 | time += ":0" + m; 307 | } 308 | if (s > 9) { 309 | time += ":" + s; 310 | } else { 311 | time += ":0" + s; 312 | } 313 | if (!callTimeView.isShown()) { 314 | callTimeView.setVisibility(View.VISIBLE); 315 | } 316 | callTimeView.setText(time); 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/VideoCallActivity.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import android.content.res.Configuration; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.hardware.Camera; 7 | import android.os.Bundle; 8 | import android.support.design.widget.FloatingActionButton; 9 | import android.view.SurfaceView; 10 | import android.view.View; 11 | import android.widget.ImageButton; 12 | import android.widget.RelativeLayout; 13 | import android.widget.TextView; 14 | import android.widget.Toast; 15 | 16 | import butterknife.BindView; 17 | import butterknife.ButterKnife; 18 | import butterknife.OnClick; 19 | 20 | import com.hyphenate.chat.EMCallStateChangeListener; 21 | import com.hyphenate.chat.EMClient; 22 | import com.hyphenate.chat.EMVideoCallHelper; 23 | import com.hyphenate.exceptions.HyphenateException; 24 | import com.hyphenate.media.EMCallSurfaceView; 25 | import com.superrtc.sdk.VideoView; 26 | import com.vmloft.develop.app.demo.call.R; 27 | import com.vmloft.develop.app.demo.call.camera.PreviewManager; 28 | import com.vmloft.develop.library.tools.utils.VMDimen; 29 | import com.vmloft.develop.library.tools.utils.VMFile; 30 | import com.vmloft.develop.library.tools.utils.VMLog; 31 | import com.vmloft.develop.library.tools.utils.VMViewUtil; 32 | 33 | import java.io.File; 34 | 35 | import org.greenrobot.eventbus.Subscribe; 36 | import org.greenrobot.eventbus.ThreadMode; 37 | 38 | /** 39 | * Created by lzan13 on 2016/10/18. 40 | * 视频通话界面处理 41 | */ 42 | public class VideoCallActivity extends CallActivity { 43 | 44 | // 视频通话帮助类 45 | private EMVideoCallHelper videoCallHelper; 46 | // SurfaceView 控件状态,-1 表示通话未接通,0 表示本小远大,1 表示远小本大 47 | private int surfaceState = -1; 48 | private boolean isMonitor = false; 49 | 50 | private int littleWidth; 51 | private int littleHeight; 52 | private int rightMargin; 53 | private int topMargin; 54 | 55 | private EMCallSurfaceView localSurface = null; 56 | private EMCallSurfaceView oppositeSurface = null; 57 | private RelativeLayout.LayoutParams localParams = null; 58 | private RelativeLayout.LayoutParams oppositeParams = null; 59 | 60 | // 使用 ButterKnife 注解的方式获取控件 61 | @BindView(R.id.layout_root) View rootView; 62 | @BindView(R.id.layout_call_control) View controlLayout; 63 | @BindView(R.id.layout_surface_container) RelativeLayout surfaceLayout; 64 | @BindView(R.id.surface_view) SurfaceView surfaceView; 65 | 66 | @BindView(R.id.btn_exit_full_screen) ImageButton exitFullScreenBtn; 67 | @BindView(R.id.text_call_state) TextView callStateView; 68 | @BindView(R.id.text_call_time) TextView callTimeView; 69 | @BindView(R.id.btn_call_info) ImageButton callInfoBtn; 70 | @BindView(R.id.text_call_info) TextView callInfoView; 71 | @BindView(R.id.btn_mic_switch) ImageButton micSwitch; 72 | @BindView(R.id.btn_camera_switch) ImageButton cameraSwitch; 73 | @BindView(R.id.btn_speaker_switch) ImageButton speakerSwitch; 74 | @BindView(R.id.btn_record_switch) ImageButton recordSwitch; 75 | @BindView(R.id.btn_screenshot) ImageButton screenshotSwitch; 76 | @BindView(R.id.btn_change_camera_switch) ImageButton changeCameraSwitch; 77 | @BindView(R.id.fab_reject_call) FloatingActionButton rejectCallFab; 78 | @BindView(R.id.fab_end_call) FloatingActionButton endCallFab; 79 | @BindView(R.id.fab_answer_call) FloatingActionButton answerCallFab; 80 | 81 | @Override 82 | protected void onCreate(Bundle savedInstanceState) { 83 | super.onCreate(savedInstanceState); 84 | setContentView(R.layout.activity_video_call); 85 | 86 | ButterKnife.bind(this); 87 | 88 | initView(); 89 | } 90 | 91 | /** 92 | * 重载父类方法,实现一些当前通话的操作, 93 | */ 94 | @Override 95 | protected void initView() { 96 | VMViewUtil.getAllChildViews(activity.getWindow().getDecorView(), 1); 97 | 98 | littleWidth = VMDimen.dp2px(96); 99 | littleHeight = VMDimen.dp2px(128); 100 | rightMargin = VMDimen.dp2px(16); 101 | topMargin = VMDimen.dp2px(96); 102 | 103 | super.initView(); 104 | if (CallManager.getInstance().isInComingCall()) { 105 | endCallFab.setVisibility(View.GONE); 106 | answerCallFab.setVisibility(View.VISIBLE); 107 | rejectCallFab.setVisibility(View.VISIBLE); 108 | callStateView.setText(R.string.call_connected_is_incoming); 109 | } else { 110 | endCallFab.setVisibility(View.VISIBLE); 111 | answerCallFab.setVisibility(View.GONE); 112 | rejectCallFab.setVisibility(View.GONE); 113 | callStateView.setText(R.string.call_connecting); 114 | } 115 | 116 | micSwitch.setActivated(!CallManager.getInstance().isOpenMic()); 117 | cameraSwitch.setActivated(!CallManager.getInstance().isOpenCamera()); 118 | speakerSwitch.setActivated(CallManager.getInstance().isOpenSpeaker()); 119 | recordSwitch.setActivated(CallManager.getInstance().isOpenRecord()); 120 | 121 | // 初始化视频通话帮助类 122 | videoCallHelper = EMClient.getInstance().callManager().getVideoCallHelper(); 123 | 124 | // 初始化显示通话画面 125 | initCallSurface(); 126 | // 判断当前通话时刚开始,还是从后台恢复已经存在的通话 127 | if (CallManager.getInstance().getCallState() == CallManager.CallState.ACCEPTED) { 128 | endCallFab.setVisibility(View.VISIBLE); 129 | answerCallFab.setVisibility(View.GONE); 130 | rejectCallFab.setVisibility(View.GONE); 131 | callStateView.setText(R.string.call_accepted); 132 | refreshCallTime(); 133 | // 通话已接通,修改画面显示 134 | onCallSurface(); 135 | } 136 | 137 | try { 138 | // 设置默认摄像头为前置 139 | EMClient.getInstance() 140 | .callManager() 141 | .setCameraFacing(Camera.CameraInfo.CAMERA_FACING_FRONT); 142 | } catch (HyphenateException e) { 143 | e.printStackTrace(); 144 | } 145 | if (CallManager.getInstance().isExternalInputData()) { 146 | new PreviewManager(surfaceView); 147 | } 148 | } 149 | 150 | /** 151 | * 界面控件点击监听器 152 | */ 153 | @OnClick({ 154 | R.id.layout_call_control, R.id.btn_exit_full_screen, R.id.btn_call_info, 155 | R.id.btn_mic_switch, R.id.btn_camera_switch, R.id.btn_speaker_switch, 156 | R.id.btn_record_switch, R.id.btn_screenshot, R.id.btn_change_camera_switch, 157 | R.id.fab_reject_call, R.id.fab_end_call, R.id.fab_answer_call 158 | }) 159 | void onClick(View v) { 160 | switch (v.getId()) { 161 | case R.id.layout_call_control: 162 | onControlLayout(); 163 | break; 164 | case R.id.btn_exit_full_screen: 165 | // 最小化通话界面 166 | exitFullScreen(); 167 | break; 168 | case R.id.btn_call_info: 169 | callInfoMonitor(); 170 | break; 171 | case R.id.btn_mic_switch: 172 | // 麦克风开关 173 | onMicrophone(); 174 | break; 175 | case R.id.btn_camera_switch: 176 | // 摄像头开关 177 | onCamera(); 178 | break; 179 | case R.id.btn_speaker_switch: 180 | // 扬声器开关 181 | onSpeaker(); 182 | break; 183 | case R.id.btn_screenshot: 184 | // 保存通话截图 185 | onScreenShot(); 186 | break; 187 | case R.id.btn_record_switch: 188 | // 录制开关 189 | onRecordCall(); 190 | break; 191 | case R.id.btn_change_camera_switch: 192 | // 切换摄像头 193 | changeCamera(); 194 | break; 195 | case R.id.fab_end_call: 196 | // 结束通话 197 | endCall(); 198 | break; 199 | case R.id.fab_reject_call: 200 | // 拒绝接听通话 201 | rejectCall(); 202 | break; 203 | case R.id.fab_answer_call: 204 | // 接听通话 205 | answerCall(); 206 | break; 207 | } 208 | } 209 | 210 | /** 211 | * 控制界面的显示与隐藏 212 | */ 213 | private void onControlLayout() { 214 | if (controlLayout.isShown()) { 215 | controlLayout.setVisibility(View.GONE); 216 | } else { 217 | controlLayout.setVisibility(View.VISIBLE); 218 | } 219 | } 220 | 221 | /** 222 | * 退出全屏通话界面 223 | */ 224 | private void exitFullScreen() { 225 | CallManager.getInstance().addFloatWindow(); 226 | // 结束当前界面 227 | onFinish(); 228 | } 229 | 230 | /** 231 | * 通话信息监听器 232 | */ 233 | private void callInfoMonitor() { 234 | if (isMonitor) { 235 | isMonitor = false; 236 | callInfoView.setVisibility(View.GONE); 237 | callInfoBtn.setActivated(isMonitor); 238 | } else { 239 | isMonitor = true; 240 | callInfoView.setVisibility(View.VISIBLE); 241 | callInfoBtn.setActivated(isMonitor); 242 | new Thread(new Runnable() { 243 | public void run() { 244 | while (isMonitor) { 245 | final String info = String.format("分辨率: %d*%d, \n延迟: %d, \n帧率: %d, \n丢失: %d, \n本地码率: %d, \n远端码率: %d, \n直连: %b", videoCallHelper 246 | .getVideoWidth(), videoCallHelper.getVideoHeight(), videoCallHelper.getVideoLatency(), videoCallHelper 247 | .getVideoFrameRate(), videoCallHelper.getVideoLostRate(), videoCallHelper 248 | .getLocalBitrate(), videoCallHelper.getRemoteBitrate(), EMClient.getInstance() 249 | .callManager() 250 | .isDirectCall()); 251 | runOnUiThread(new Runnable() { 252 | public void run() { 253 | callInfoView.setText(info); 254 | } 255 | }); 256 | try { 257 | Thread.sleep(1500); 258 | } catch (InterruptedException e) { 259 | } 260 | } 261 | } 262 | }).start(); 263 | } 264 | } 265 | 266 | /** 267 | * 麦克风开关,主要调用环信语音数据传输方法 268 | */ 269 | private void onMicrophone() { 270 | try { 271 | // 根据麦克风开关是否被激活来进行判断麦克风状态,然后进行下一步操作 272 | if (micSwitch.isActivated()) { 273 | // 设置按钮状态 274 | micSwitch.setActivated(false); 275 | // 暂停语音数据的传输 276 | EMClient.getInstance().callManager().resumeVoiceTransfer(); 277 | CallManager.getInstance().setOpenMic(true); 278 | } else { 279 | // 设置按钮状态 280 | micSwitch.setActivated(true); 281 | // 恢复语音数据的传输 282 | EMClient.getInstance().callManager().pauseVoiceTransfer(); 283 | CallManager.getInstance().setOpenMic(false); 284 | } 285 | } catch (HyphenateException e) { 286 | VMLog.e("exception code: %d, %s", e.getErrorCode(), e.getMessage()); 287 | e.printStackTrace(); 288 | } 289 | } 290 | 291 | /** 292 | * 摄像头开关 293 | */ 294 | private void onCamera() { 295 | try { 296 | // 根据摄像头开关按钮状态判断摄像头状态,然后进行下一步操作 297 | if (cameraSwitch.isActivated()) { 298 | // 设置按钮状态 299 | cameraSwitch.setActivated(false); 300 | // 暂停视频数据的传输 301 | EMClient.getInstance().callManager().resumeVideoTransfer(); 302 | CallManager.getInstance().setOpenCamera(true); 303 | } else { 304 | // 设置按钮状态 305 | cameraSwitch.setActivated(true); 306 | // 恢复视频数据的传输 307 | EMClient.getInstance().callManager().pauseVideoTransfer(); 308 | CallManager.getInstance().setOpenCamera(false); 309 | } 310 | } catch (HyphenateException e) { 311 | VMLog.e("exception code: %d, %s", e.getErrorCode(), e.getMessage()); 312 | e.printStackTrace(); 313 | } 314 | } 315 | 316 | /** 317 | * 扬声器开关 318 | */ 319 | private void onSpeaker() { 320 | // 根据按钮状态决定打开还是关闭扬声器 321 | if (speakerSwitch.isActivated()) { 322 | // 设置按钮状态 323 | speakerSwitch.setActivated(false); 324 | CallManager.getInstance().closeSpeaker(); 325 | CallManager.getInstance().setOpenSpeaker(false); 326 | } else { 327 | // 设置按钮状态 328 | speakerSwitch.setActivated(true); 329 | CallManager.getInstance().openSpeaker(); 330 | CallManager.getInstance().setOpenSpeaker(true); 331 | } 332 | } 333 | 334 | /** 335 | * 录制视屏通话内容 336 | */ 337 | private void onRecordCall() { 338 | // 根据开关状态决定是否开启录制 339 | if (recordSwitch.isActivated()) { 340 | // 设置按钮状态 341 | recordSwitch.setActivated(false); 342 | String path = videoCallHelper.stopVideoRecord(); 343 | CallManager.getInstance().setOpenRecord(false); 344 | File file = new File(path); 345 | if (file.exists()) { 346 | Toast.makeText(activity, "录制视频成功 " + path, Toast.LENGTH_LONG).show(); 347 | } else { 348 | Toast.makeText(activity, "录制失败/(ㄒoㄒ)/~~", Toast.LENGTH_LONG).show(); 349 | } 350 | } else { 351 | // 设置按钮状态 352 | recordSwitch.setActivated(true); 353 | // 先创建文件夹 354 | String dirPath = getExternalFilesDir("").getAbsolutePath() + "/videos"; 355 | File dir = new File(dirPath); 356 | if (!dir.isDirectory()) { 357 | dir.mkdirs(); 358 | } 359 | videoCallHelper.startVideoRecord(dirPath); 360 | VMLog.d("开始录制视频"); 361 | Toast.makeText(activity, "开始录制", Toast.LENGTH_LONG).show(); 362 | CallManager.getInstance().setOpenRecord(true); 363 | } 364 | } 365 | 366 | /** 367 | * 保存通话截图 368 | */ 369 | private void onScreenShot() { 370 | String dirPath = VMFile.getFilesFromSDCard() + "videos/"; 371 | File dir = new File(dirPath); 372 | if (!dir.isDirectory()) { 373 | dir.mkdirs(); 374 | } 375 | String path = dirPath + "IMG_" + System.currentTimeMillis() + ".jpg"; 376 | videoCallHelper.takePicture(path); 377 | Toast.makeText(activity, "拍照保存成功 " + path, Toast.LENGTH_LONG).show(); 378 | Bitmap bitmap = BitmapFactory.decodeFile(path); 379 | // testImgView.setImageBitmap(bitmap); 380 | // testImgView.setVisibility(View.VISIBLE); 381 | // testImgView.setOnClickListener(new View.OnClickListener() { 382 | // @Override public void onClick(View v) { 383 | // testImgView.setVisibility(View.GONE); 384 | // } 385 | // }); 386 | } 387 | 388 | /** 389 | * 切换摄像头 390 | */ 391 | private void changeCamera() { 392 | // 根据切换摄像头开关是否被激活确定当前是前置还是后置摄像头 393 | try { 394 | if (EMClient.getInstance().callManager().getCameraFacing() == 1) { 395 | EMClient.getInstance().callManager().switchCamera(); 396 | EMClient.getInstance().callManager().setCameraFacing(0); 397 | } else { 398 | EMClient.getInstance().callManager().switchCamera(); 399 | EMClient.getInstance().callManager().setCameraFacing(1); 400 | } 401 | } catch (HyphenateException e) { 402 | e.printStackTrace(); 403 | } 404 | } 405 | 406 | /** 407 | * 接听通话 408 | */ 409 | @Override 410 | protected void answerCall() { 411 | super.answerCall(); 412 | endCallFab.setVisibility(View.VISIBLE); 413 | rejectCallFab.setVisibility(View.GONE); 414 | answerCallFab.setVisibility(View.GONE); 415 | } 416 | 417 | /** 418 | * 初始化通话界面控件 419 | */ 420 | private void initCallSurface() { 421 | // 初始化显示远端画面控件 422 | oppositeSurface = new EMCallSurfaceView(activity); 423 | oppositeParams = new RelativeLayout.LayoutParams(0, 0); 424 | oppositeParams.width = RelativeLayout.LayoutParams.MATCH_PARENT; 425 | oppositeParams.height = RelativeLayout.LayoutParams.MATCH_PARENT; 426 | oppositeSurface.setLayoutParams(oppositeParams); 427 | surfaceLayout.addView(oppositeSurface); 428 | 429 | // 初始化显示本地画面控件 430 | localSurface = new EMCallSurfaceView(activity); 431 | localParams = new RelativeLayout.LayoutParams(0, 0); 432 | localParams.width = RelativeLayout.LayoutParams.MATCH_PARENT; 433 | localParams.height = RelativeLayout.LayoutParams.MATCH_PARENT; 434 | localParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); 435 | localSurface.setLayoutParams(localParams); 436 | surfaceLayout.addView(localSurface); 437 | 438 | localSurface.setOnClickListener(new View.OnClickListener() { 439 | @Override 440 | public void onClick(View v) { 441 | onControlLayout(); 442 | } 443 | }); 444 | 445 | localSurface.setZOrderOnTop(false); 446 | localSurface.setZOrderMediaOverlay(true); 447 | 448 | // 设置本地和远端画面的显示方式,是填充,还是居中 449 | localSurface.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFit); 450 | oppositeSurface.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFit); 451 | // 设置通话画面显示控件 452 | EMClient.getInstance().callManager().setSurfaceView(localSurface, oppositeSurface); 453 | } 454 | 455 | /** 456 | * 接通通话,这个时候要做的只是改变本地画面 view 大小,不需要做其他操作 457 | */ 458 | private void onCallSurface() { 459 | // 更新通话界面控件状态 460 | surfaceState = 0; 461 | 462 | localParams = new RelativeLayout.LayoutParams(littleWidth, littleHeight); 463 | localParams.width = littleWidth; 464 | localParams.height = littleHeight; 465 | localParams.rightMargin = rightMargin; 466 | localParams.topMargin = topMargin; 467 | localParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); 468 | localSurface.setLayoutParams(localParams); 469 | 470 | localSurface.setOnClickListener(new View.OnClickListener() { 471 | @Override 472 | public void onClick(View v) { 473 | changeCallSurface(); 474 | } 475 | }); 476 | 477 | oppositeSurface.setOnClickListener(new View.OnClickListener() { 478 | @Override 479 | public void onClick(View v) { 480 | onControlLayout(); 481 | } 482 | }); 483 | } 484 | 485 | /** 486 | * 切换通话界面,这里就是交换本地和远端画面控件设置,以达到通话大小画面的切换 487 | */ 488 | private void changeCallSurface() { 489 | if (surfaceState == 0) { 490 | surfaceState = 1; 491 | EMClient.getInstance().callManager().setSurfaceView(oppositeSurface, localSurface); 492 | } else { 493 | surfaceState = 0; 494 | EMClient.getInstance().callManager().setSurfaceView(localSurface, oppositeSurface); 495 | } 496 | } 497 | 498 | @Subscribe(threadMode = ThreadMode.MAIN) 499 | public void onEventBus(CallEvent event) { 500 | if (event.isState()) { 501 | refreshCallView(event); 502 | } 503 | if (event.isTime()) { 504 | // 不论什么情况都检查下当前时间 505 | refreshCallTime(); 506 | } 507 | } 508 | 509 | /** 510 | * 刷新通话界面 511 | */ 512 | private void refreshCallView(CallEvent event) { 513 | EMCallStateChangeListener.CallError callError = event.getCallError(); 514 | EMCallStateChangeListener.CallState callState = event.getCallState(); 515 | switch (callState) { 516 | case CONNECTING: // 正在呼叫对方,TODO 没见回调过 517 | VMLog.i("正在呼叫对方" + callError); 518 | break; 519 | case CONNECTED: // 正在等待对方接受呼叫申请(对方申请与你进行通话) 520 | VMLog.i("正在连接" + callError); 521 | if (CallManager.getInstance().isInComingCall()) { 522 | callStateView.setText(R.string.call_connected_is_incoming); 523 | } else { 524 | callStateView.setText(R.string.call_connected); 525 | } 526 | break; 527 | case ACCEPTED: // 通话已接通 528 | VMLog.i("通话已接通"); 529 | callStateView.setText(R.string.call_accepted); 530 | // 通话接通,更新界面 UI 显示 531 | onCallSurface(); 532 | break; 533 | case DISCONNECTED: // 通话已中断 534 | VMLog.i("通话已结束" + callError); 535 | onFinish(); 536 | break; 537 | case NETWORK_DISCONNECTED: 538 | Toast.makeText(activity, "对方网络断开", Toast.LENGTH_SHORT).show(); 539 | VMLog.i("对方网络断开"); 540 | break; 541 | case NETWORK_NORMAL: 542 | VMLog.i("网络正常"); 543 | break; 544 | case NETWORK_UNSTABLE: 545 | if (callError == EMCallStateChangeListener.CallError.ERROR_NO_DATA) { 546 | VMLog.i("没有通话数据" + callError); 547 | } else { 548 | VMLog.i("网络不稳定" + callError); 549 | } 550 | break; 551 | case VIDEO_PAUSE: 552 | Toast.makeText(activity, "对方已暂停视频传输", Toast.LENGTH_SHORT).show(); 553 | VMLog.i("对方已暂停视频传输"); 554 | break; 555 | case VIDEO_RESUME: 556 | Toast.makeText(activity, "对方已恢复视频传输", Toast.LENGTH_SHORT).show(); 557 | VMLog.i("对方已恢复视频传输"); 558 | break; 559 | case VOICE_PAUSE: 560 | Toast.makeText(activity, "对方已暂停语音传输", Toast.LENGTH_SHORT).show(); 561 | VMLog.i("对方已暂停语音传输"); 562 | break; 563 | case VOICE_RESUME: 564 | Toast.makeText(activity, "对方已恢复语音传输", Toast.LENGTH_SHORT).show(); 565 | VMLog.i("对方已恢复语音传输"); 566 | break; 567 | default: 568 | break; 569 | } 570 | } 571 | 572 | /** 573 | * 刷新通话时间显示 574 | */ 575 | private void refreshCallTime() { 576 | int t = CallManager.getInstance().getCallTime(); 577 | int h = t / 60 / 60; 578 | int m = t / 60 % 60; 579 | int s = t % 60 % 60; 580 | String time = ""; 581 | if (h > 9) { 582 | time = "" + h; 583 | } else { 584 | time = "0" + h; 585 | } 586 | if (m > 9) { 587 | time += ":" + m; 588 | } else { 589 | time += ":0" + m; 590 | } 591 | if (s > 9) { 592 | time += ":" + s; 593 | } else { 594 | time += ":0" + s; 595 | } 596 | if (!callTimeView.isShown()) { 597 | callTimeView.setVisibility(View.VISIBLE); 598 | } 599 | callTimeView.setText(time); 600 | } 601 | 602 | /** 603 | * 屏幕方向改变回调方法 604 | */ 605 | @Override 606 | public void onConfigurationChanged(Configuration newConfig) { 607 | super.onConfigurationChanged(newConfig); 608 | } 609 | 610 | @Override 611 | protected void onUserLeaveHint() { 612 | //super.onUserLeaveHint(); 613 | exitFullScreen(); 614 | } 615 | 616 | /** 617 | * 通话界面拦截 Back 按键,不能返回 618 | */ 619 | @Override 620 | public void onBackPressed() { 621 | //super.onBackPressed(); 622 | exitFullScreen(); 623 | } 624 | 625 | @Override 626 | public void onFinish() { 627 | // release surface view 628 | if (localSurface != null) { 629 | if (localSurface.getRenderer() != null) { 630 | localSurface.getRenderer().dispose(); 631 | } 632 | localSurface.release(); 633 | localSurface = null; 634 | } 635 | if (oppositeSurface != null) { 636 | if (oppositeSurface.getRenderer() != null) { 637 | oppositeSurface.getRenderer().dispose(); 638 | } 639 | oppositeSurface.release(); 640 | oppositeSurface = null; 641 | } 642 | super.onFinish(); 643 | } 644 | } 645 | -------------------------------------------------------------------------------- /app/src/main/java/com/vmloft/develop/app/demo/call/single/VoiceCallActivity.java: -------------------------------------------------------------------------------- 1 | package com.vmloft.develop.app.demo.call.single; 2 | 3 | import android.content.res.Configuration; 4 | import android.os.Bundle; 5 | import android.support.design.widget.FloatingActionButton; 6 | import android.support.design.widget.Snackbar; 7 | import android.view.View; 8 | import android.widget.ImageButton; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | import android.widget.Toast; 12 | import butterknife.BindView; 13 | import butterknife.ButterKnife; 14 | import butterknife.OnClick; 15 | 16 | import com.hyphenate.chat.EMCallStateChangeListener; 17 | import com.hyphenate.chat.EMClient; 18 | import com.hyphenate.exceptions.HyphenateException; 19 | 20 | import com.vmloft.develop.app.demo.call.R; 21 | import com.vmloft.develop.library.tools.utils.VMLog; 22 | import org.greenrobot.eventbus.Subscribe; 23 | import org.greenrobot.eventbus.ThreadMode; 24 | 25 | /** 26 | * Created by lzan13 on 2016/10/18. 27 | * 28 | * 音频通话界面处理 29 | */ 30 | public class VoiceCallActivity extends CallActivity { 31 | 32 | // 使用 ButterKnife 注解的方式获取控件 33 | @BindView(R.id.layout_root) View rootView; 34 | @BindView(R.id.text_call_state) TextView callStateView; 35 | @BindView(R.id.text_call_time) TextView callTimeView; 36 | @BindView(R.id.img_call_avatar) ImageView avatarView; 37 | @BindView(R.id.text_call_username) TextView usernameView; 38 | @BindView(R.id.btn_exit_full_screen) ImageButton exitFullScreenBtn; 39 | @BindView(R.id.btn_mic_switch) ImageButton micSwitch; 40 | @BindView(R.id.btn_speaker_switch) ImageButton speakerSwitch; 41 | @BindView(R.id.btn_record_switch) ImageButton recordSwitch; 42 | @BindView(R.id.fab_reject_call) FloatingActionButton rejectCallFab; 43 | @BindView(R.id.fab_end_call) FloatingActionButton endCallFab; 44 | @BindView(R.id.fab_answer_call) FloatingActionButton answerCallFab; 45 | 46 | @Override protected void onCreate(Bundle savedInstanceState) { 47 | super.onCreate(savedInstanceState); 48 | setContentView(R.layout.activity_voice_call); 49 | 50 | ButterKnife.bind(this); 51 | 52 | initView(); 53 | } 54 | 55 | /** 56 | * 重载父类方法,实现一些当前通话的操作, 57 | */ 58 | @Override protected void initView() { 59 | super.initView(); 60 | if (CallManager.getInstance().isInComingCall()) { 61 | endCallFab.setVisibility(View.GONE); 62 | answerCallFab.setVisibility(View.VISIBLE); 63 | rejectCallFab.setVisibility(View.VISIBLE); 64 | callStateView.setText(R.string.call_connected_is_incoming); 65 | } else { 66 | endCallFab.setVisibility(View.VISIBLE); 67 | answerCallFab.setVisibility(View.GONE); 68 | rejectCallFab.setVisibility(View.GONE); 69 | callStateView.setText(R.string.call_connecting); 70 | } 71 | 72 | usernameView.setText(CallManager.getInstance().getChatId()); 73 | 74 | micSwitch.setActivated(!CallManager.getInstance().isOpenMic()); 75 | speakerSwitch.setActivated(CallManager.getInstance().isOpenSpeaker()); 76 | recordSwitch.setActivated(CallManager.getInstance().isOpenRecord()); 77 | 78 | // 判断当前通话时刚开始,还是从后台恢复已经存在的通话 79 | if (CallManager.getInstance().getCallState() == CallManager.CallState.ACCEPTED) { 80 | endCallFab.setVisibility(View.VISIBLE); 81 | answerCallFab.setVisibility(View.GONE); 82 | rejectCallFab.setVisibility(View.GONE); 83 | callStateView.setText(R.string.call_accepted); 84 | refreshCallTime(); 85 | } 86 | } 87 | 88 | /** 89 | * 界面控件点击监听器 90 | */ 91 | @OnClick({ 92 | R.id.btn_exit_full_screen, R.id.btn_mic_switch, R.id.btn_speaker_switch, R.id.btn_record_switch, R.id.fab_reject_call, 93 | R.id.fab_end_call, R.id.fab_answer_call 94 | }) void onClick(View v) { 95 | switch (v.getId()) { 96 | case R.id.btn_exit_full_screen: 97 | // 最小化通话界面 98 | exitFullScreen(); 99 | break; 100 | case R.id.btn_mic_switch: 101 | // 麦克风开关 102 | onMicrophone(); 103 | break; 104 | case R.id.btn_speaker_switch: 105 | // 扬声器开关 106 | onSpeaker(); 107 | break; 108 | case R.id.btn_record_switch: 109 | // 录制开关 110 | recordCall(); 111 | break; 112 | case R.id.fab_end_call: 113 | // 结束通话 114 | endCall(); 115 | break; 116 | case R.id.fab_reject_call: 117 | // 拒绝接听通话 118 | rejectCall(); 119 | break; 120 | case R.id.fab_answer_call: 121 | // 接听通话 122 | answerCall(); 123 | break; 124 | } 125 | } 126 | 127 | /** 128 | * 接听通话 129 | */ 130 | @Override protected void answerCall() { 131 | super.answerCall(); 132 | 133 | endCallFab.setVisibility(View.VISIBLE); 134 | rejectCallFab.setVisibility(View.GONE); 135 | answerCallFab.setVisibility(View.GONE); 136 | } 137 | 138 | /** 139 | * 退出全屏通话界面 140 | */ 141 | private void exitFullScreen() { 142 | CallManager.getInstance().addFloatWindow(); 143 | onFinish(); 144 | } 145 | 146 | /** 147 | * 麦克风开关,主要调用环信语音数据传输方法 148 | */ 149 | private void onMicrophone() { 150 | try { 151 | // 根据麦克风开关是否被激活来进行判断麦克风状态,然后进行下一步操作 152 | if (micSwitch.isActivated()) { 153 | // 设置按钮状态 154 | micSwitch.setActivated(false); 155 | // 暂停语音数据的传输 156 | EMClient.getInstance().callManager().resumeVoiceTransfer(); 157 | CallManager.getInstance().setOpenMic(true); 158 | } else { 159 | // 设置按钮状态 160 | micSwitch.setActivated(true); 161 | // 恢复语音数据的传输 162 | EMClient.getInstance().callManager().pauseVoiceTransfer(); 163 | CallManager.getInstance().setOpenMic(false); 164 | } 165 | } catch (HyphenateException e) { 166 | VMLog.e("exception code: %d, %s", e.getErrorCode(), e.getMessage()); 167 | e.printStackTrace(); 168 | } 169 | } 170 | 171 | /** 172 | * 扬声器开关 173 | */ 174 | private void onSpeaker() { 175 | // 根据按钮状态决定打开还是关闭扬声器 176 | if (speakerSwitch.isActivated()) { 177 | // 设置按钮状态 178 | speakerSwitch.setActivated(false); 179 | CallManager.getInstance().closeSpeaker(); 180 | CallManager.getInstance().setOpenSpeaker(false); 181 | } else { 182 | // 设置按钮状态 183 | speakerSwitch.setActivated(true); 184 | CallManager.getInstance().openSpeaker(); 185 | CallManager.getInstance().setOpenSpeaker(true); 186 | } 187 | } 188 | 189 | /** 190 | * 录制通话内容 TODO 后期实现 191 | */ 192 | private void recordCall() { 193 | Snackbar.make(rootView, "暂未实现", Snackbar.LENGTH_LONG).show(); 194 | // 根据开关状态决定是否开启录制 195 | if (recordSwitch.isActivated()) { 196 | // 设置按钮状态 197 | recordSwitch.setActivated(false); 198 | CallManager.getInstance().setOpenRecord(false); 199 | } else { 200 | // 设置按钮状态 201 | recordSwitch.setActivated(true); 202 | CallManager.getInstance().setOpenRecord(true); 203 | } 204 | } 205 | 206 | @Subscribe(threadMode = ThreadMode.MAIN) public void onEventBus(CallEvent event) { 207 | if (event.isState()) { 208 | refreshCallView(event); 209 | } 210 | if (event.isTime()) { 211 | // 不论什么情况都检查下当前时间 212 | refreshCallTime(); 213 | } 214 | } 215 | 216 | /** 217 | * 刷新通话界面 218 | */ 219 | private void refreshCallView(CallEvent event) { 220 | EMCallStateChangeListener.CallError callError = event.getCallError(); 221 | EMCallStateChangeListener.CallState callState = event.getCallState(); 222 | switch (callState) { 223 | case CONNECTING: // 正在呼叫对方,TODO 没见回调过 224 | VMLog.i("正在呼叫对方" + callError); 225 | break; 226 | case CONNECTED: // 正在等待对方接受呼叫申请(对方申请与你进行通话) 227 | VMLog.i("正在连接" + callError); 228 | runOnUiThread(new Runnable() { 229 | @Override public void run() { 230 | if (CallManager.getInstance().isInComingCall()) { 231 | callStateView.setText(R.string.call_connected_is_incoming); 232 | } else { 233 | callStateView.setText(R.string.call_connected); 234 | } 235 | } 236 | }); 237 | break; 238 | case ACCEPTED: // 通话已接通 239 | VMLog.i("通话已接通"); 240 | runOnUiThread(new Runnable() { 241 | @Override public void run() { 242 | callStateView.setText(R.string.call_accepted); 243 | } 244 | }); 245 | break; 246 | case DISCONNECTED: // 通话已中断 247 | VMLog.i("通话已结束" + callError); 248 | onFinish(); 249 | break; 250 | case NETWORK_DISCONNECTED: 251 | Toast.makeText(activity, "对方网络断开", Toast.LENGTH_SHORT).show(); 252 | VMLog.i("对方网络断开"); 253 | break; 254 | case NETWORK_NORMAL: 255 | VMLog.i("网络正常"); 256 | break; 257 | case NETWORK_UNSTABLE: 258 | if (callError == EMCallStateChangeListener.CallError.ERROR_NO_DATA) { 259 | VMLog.i("没有通话数据" + callError); 260 | } else { 261 | VMLog.i("网络不稳定" + callError); 262 | } 263 | break; 264 | case VIDEO_PAUSE: 265 | Toast.makeText(activity, "对方已暂停视频传输", Toast.LENGTH_SHORT).show(); 266 | VMLog.i("对方已暂停视频传输"); 267 | break; 268 | case VIDEO_RESUME: 269 | Toast.makeText(activity, "对方已恢复视频传输", Toast.LENGTH_SHORT).show(); 270 | VMLog.i("对方已恢复视频传输"); 271 | break; 272 | case VOICE_PAUSE: 273 | Toast.makeText(activity, "对方已暂停语音传输", Toast.LENGTH_SHORT).show(); 274 | VMLog.i("对方已暂停语音传输"); 275 | break; 276 | case VOICE_RESUME: 277 | Toast.makeText(activity, "对方已恢复语音传输", Toast.LENGTH_SHORT).show(); 278 | VMLog.i("对方已恢复语音传输"); 279 | break; 280 | default: 281 | break; 282 | } 283 | } 284 | 285 | /** 286 | * 刷新通话时间显示 287 | */ 288 | private void refreshCallTime() { 289 | int t = CallManager.getInstance().getCallTime(); 290 | int h = t / 60 / 60; 291 | int m = t / 60 % 60; 292 | int s = t % 60 % 60; 293 | String time = ""; 294 | if (h > 9) { 295 | time = "" + h; 296 | } else { 297 | time = "0" + h; 298 | } 299 | if (m > 9) { 300 | time += ":" + m; 301 | } else { 302 | time += ":0" + m; 303 | } 304 | if (s > 9) { 305 | time += ":" + s; 306 | } else { 307 | time += ":0" + s; 308 | } 309 | if (!callTimeView.isShown()) { 310 | callTimeView.setVisibility(View.VISIBLE); 311 | } 312 | callTimeView.setText(time); 313 | } 314 | 315 | /** 316 | * 屏幕方向改变回调方法 317 | */ 318 | @Override public void onConfigurationChanged(Configuration newConfig) { 319 | super.onConfigurationChanged(newConfig); 320 | } 321 | 322 | @Override protected void onUserLeaveHint() { 323 | //super.onUserLeaveHint(); 324 | exitFullScreen(); 325 | } 326 | 327 | /** 328 | * 通话界面拦截 Back 按键,不能返回 329 | */ 330 | @Override public void onBackPressed() { 331 | //super.onBackPressed(); 332 | exitFullScreen(); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_camera_change_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/drawable-xxhdpi/ic_camera_change_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_character_blackcat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/drawable-xxhdpi/ic_character_blackcat.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_character_mustache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/drawable-xxhdpi/ic_character_mustache.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_character_penguin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/drawable-xxhdpi/ic_character_penguin.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_character_spider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/drawable-xxhdpi/ic_character_spider.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/click_circle_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/click_circle_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/click_circle_transparent.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_call_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_call_end_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_camera_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_fullscreen_exit_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_outline_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_off_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_record_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_screen_share_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_videocam_off_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_volume_up_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_conference.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 18 | 19 | 24 | 25 | 26 | 27 | 32 | 33 | 34 | 38 | 39 | 47 | 48 | 54 | 55 | 56 | 66 | 67 | 68 | 76 | 77 | 78 | 86 | 87 | 92 | 93 | 94 | 101 | 102 | 103 | 110 | 111 | 112 | 119 | 120 | 121 | 128 | 129 | 130 | 139 | 140 | 141 | 146 | 147 | 148 | 157 | 158 | 159 | 168 | 169 | 170 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 17 | 18 | 24 | 25 | 34 | 35 | 36 | 42 | 43 | 52 | 53 | 54 | 60 | 61 | 70 | 71 | 72 | 75 | 76 | 80 | 81 | 82 | 85 | 86 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_video_call.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 18 | 19 | 23 | 24 | 25 | 30 | 31 | 32 | 36 | 37 | 45 | 46 | 52 | 53 | 54 | 63 | 64 | 74 | 75 | 76 | 84 | 85 | 86 | 87 | 95 | 96 | 100 | 101 | 106 | 107 | 108 | 113 | 114 | 115 | 121 | 122 | 123 | 129 | 130 | 131 | 137 | 138 | 139 | 145 | 146 | 147 | 153 | 154 | 162 | 163 | 164 | 169 | 170 | 171 | 180 | 181 | 182 | 192 | 193 | 194 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_voice_call.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 21 | 22 | 28 | 29 | 34 | 35 | 44 | 51 | 52 | 53 | 63 | 64 | 75 | 76 | 77 | 78 | 85 | 86 | 92 | 93 | 103 | 104 | 105 | 114 | 115 | 120 | 121 | 122 | 130 | 131 | 132 | 140 | 141 | 142 | 150 | 151 | 152 | 153 | 158 | 159 | 169 | 170 | 171 | 182 | 183 | 184 | 194 | 195 | 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_conference_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 18 | 19 | 24 | 25 | 32 | 33 | 44 | 45 | 56 | 57 | -------------------------------------------------------------------------------- /app/src/main/res/layout/widget_float_window.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 23 | 24 | 31 | 32 | 33 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/raw/em_outgoing.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/raw/em_outgoing.ogg -------------------------------------------------------------------------------- /app/src/main/res/raw/sound_call_incoming.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/raw/sound_call_incoming.mp3 -------------------------------------------------------------------------------- /app/src/main/res/raw/sound_calling.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzan13/VMChatDemoCall/1eac72012ce34f49e79561f1071a19ad1716be50/app/src/main/res/raw/sound_calling.mp3 -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #ffffff 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | VMChatDemoCall 3 | 4 | 5 | 通话已接通 6 | 对方正忙 7 | 已取消 8 | 对方已取消 9 | 正在呼叫对方 10 | 正在等待对方接受呼叫申请 11 | 对方申请与你进行通话 12 | 建立连接失败 13 | 通话已结束 14 | 通话时长 15 | 没有通话数据 16 | 对方未接听 17 | 对方可能在忙,建议稍后拨打 18 | 对方不在线 19 | 网络状态不佳 20 | 网络状态良好 21 | 对方已拒绝 22 | 已拒绝 23 | 本地版本过低,无法通讯 24 | 对方版本过低,无法通讯 25 | 双方通讯协议不同,无法通讯 26 | 视频传输已暂停 27 | 视频传输已恢复 28 | 视频通话进行中 29 | 语音传输已暂停 30 | 语音传输已恢复 31 | 语音通话进行中 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |