├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── music.mp3 │ ├── java │ └── com │ │ └── wzh │ │ └── p2ptest │ │ ├── JavaTest.java │ │ ├── RtcActivity.java │ │ ├── ScreenActivity.java │ │ ├── Utils.java │ │ └── WebRtcClient.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── aaa.png │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_main.xml │ ├── activity_screen.xml │ └── main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img └── aa.png ├── local.properties └── settings.gradle /README.md: -------------------------------------------------------------------------------- 1 | # AndroidP2pTest 2 | 两个android客户端进行点对点(p2p)及时通信,通信内容可以文字,图片,文件。 3 | 4 | 快速开始 5 | 6 | 运行项目,在log中查看到 7 | ``` 8 | onId data=Nzs3jmD5g876WCpcAAEH 9 | ``` 10 | 其中data后为当前设备id,id 每次运行都会变,把id赋值给 11 | RtcActivity.callerId ,运行到另一台设备上,启动后会自己建立连接 12 | 13 | 待log出现以下输出,表明连接建立, 14 | ``` 15 | registerObserver onStateChangeOPEN 16 | Peer onDataChannel=org.webrtc.DataChannel@bf04e5c 17 | ``` 18 | 可在输入框输入文字,->发送,另一设备将会收到并显示,发送图片,发送文件按钮一样,内部已处理64k 自动分包问题 19 | 详细参考我的文章:[Android P2P 通信方案探索](https://blog.csdn.net/u010521645/article/details/81512549) 20 | 21 | ![Image text](https://raw.githubusercontent.com/KgdFnYpeu/AndroidP2pTest/805725df89b2afaf3e979ee9310370dd6bfeb434/img/aa.png) 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "26.0.2" 6 | 7 | defaultConfig { 8 | applicationId "com.wzh.p2ptest" 9 | minSdkVersion 19 10 | targetSdkVersion 25 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | implementation 'com.android.support:appcompat-v7:25.3.1' 25 | 26 | implementation 'com.android.support.constraint:constraint-layout:1.1.2' 27 | compile 'com.github.nkzawa:socket.io-client:0.4.2' 28 | compile 'io.pristine:libjingle:8871@aar' 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/assets/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zihao-Wu/AndroidP2pTest/53ac2be5315096ffc4e803c2b2aa892df3a85243/app/src/main/assets/music.mp3 -------------------------------------------------------------------------------- /app/src/main/java/com/wzh/p2ptest/JavaTest.java: -------------------------------------------------------------------------------- 1 | package com.wzh.p2ptest; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * Created by wzh on 07/08/2018. 7 | */ 8 | 9 | public class JavaTest { 10 | 11 | public static void main(String[] args) { 12 | int i = 2147483647; 13 | String bin = Integer.toBinaryString(16384); 14 | System.out.println("ll: " + i + " " + bin); 15 | 16 | i = 1073741824; 17 | System.out.println("ll: " + i + " " + Integer.toBinaryString(i)); 18 | 19 | 20 | int size = 16384; 21 | 22 | byte b = (byte) size; 23 | System.out.println("b: " + b); 24 | 25 | byte[] data = new byte[992]; 26 | long setV = 34359738369L; 27 | setValue(data, 0, 5, setV); 28 | System.out.println("setv: "+setV); 29 | 30 | long getV = getValue(data, 0, 5); 31 | System.out.println("getValue: " + setV + " == " + getV + " ?=" + (setV == getV)); 32 | 33 | i = 10; 34 | i = i << 0;//左移,就是在二进制中,在右边添加n个0 ,如3<<2 =11 ,00 = 12 35 | System.out.println("i: " + i); 36 | 37 | i = 15; 38 | i = i >>> 2; 39 | System.out.println("i: " + i); 40 | 41 | 42 | i = i >> 0;//右移,就是删除二进制中的右面n个 如15>>2 = 11,11 删除后面11 = 3 43 | System.out.println("i: " + i); 44 | 45 | 46 | } 47 | 48 | 49 | private static void setIndex(byte[] data, int index) { 50 | if (index <= Byte.MAX_VALUE) {//byte -128~127 51 | data[0] = 0; 52 | data[1] = (byte) index; 53 | } else { 54 | String binary = Integer.toBinaryString(index);//二进制 55 | System.out.println(": " + binary); 56 | int len = binary.length(); 57 | data[0] = (byte) (index >> 7);//>>右移,丢弃右边7位 58 | // data[0]=Byte.parseByte(binary.substring(0,len-7),2); 59 | data[1] = Byte.parseByte(binary.substring(len - 7), 2); 60 | } 61 | System.out.println(String.format("index [0]=%d ,[1]=%d ", data[0], data[1])); 62 | 63 | } 64 | 65 | 66 | private static int getIndex(byte[] data) { 67 | if (data[0] == 0) 68 | return data[1]; 69 | String high = Integer.toBinaryString(data[0]); 70 | String low = Integer.toBinaryString(data[1]); 71 | System.out.println("[0]: " + high + " [1]" + low); 72 | String binary = high 73 | + String.format(Locale.CHINA, "%07d", Integer.parseInt(low)); 74 | System.out.println("getIndex: " + binary); 75 | return Integer.parseInt(binary, 2); 76 | } 77 | 78 | /** 79 | * 把int弄value 值 转换成byte数组 表示 80 | * 如 setValue(data,0,2,333) 表示在数组中0,1两个byte 中分隔存放 333 81 | * 82 | * @param data 83 | * @param firstPosition 数据存放开始索引 84 | * @param endPosition 数据存放结束索引位置,在数组不包含此位置,如 firstPosition=0,endPosition=2,表示 0,1索引 85 | * @param value 要表示的int值 86 | */ 87 | private static void setValue(byte[] data, int firstPosition, int endPosition, long value) { 88 | if (endPosition - firstPosition < 1) 89 | throw new IndexOutOfBoundsException("end - first must > 0 " + endPosition + " - " + firstPosition); 90 | 91 | if (value <= Byte.MAX_VALUE) {//byte -128~127 92 | for (int i = firstPosition; i < endPosition - 1; i++) { 93 | data[i] = 0; 94 | } 95 | data[endPosition - 1] = (byte) value; 96 | } else { 97 | String binary = Long.toBinaryString(value);//二进制 98 | System.out.println(": " + binary); 99 | int len = binary.length(); 100 | int maxBit = (endPosition - firstPosition) * 7; 101 | if (len > maxBit) {//超出最大可表示数 102 | throw new IndexOutOfBoundsException(value + "--> " + binary + " out of " + maxBit + " bit byte value"); 103 | } 104 | 105 | for (int i = endPosition - 1; i >= firstPosition; i--) { 106 | long newValue = value >> 7;//去除后七位 107 | data[i] = (byte) (value - (newValue << 7));//加上七位0,相减就是后七位值 108 | value = newValue; 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * 把fir~end 位的数转换为对应value 115 | * @param data 116 | * @param firstPosition 117 | * @param endPosition 如fir=0,end=2,表示0,1两位,不包含索引2 118 | * @return 119 | */ 120 | private static long getValue(byte[] data, int firstPosition, int endPosition) { 121 | if (endPosition - firstPosition < 1) 122 | throw new IndexOutOfBoundsException("end - first must > 0 " + endPosition + " - " + firstPosition); 123 | 124 | long value = 0; 125 | int bit = endPosition - firstPosition - 1; 126 | for (int i = firstPosition; i < endPosition; i++, bit--) { 127 | long bitValue = (long) data[i] << (7 * bit); 128 | // System.out.println("bit: " + bit + " bitV=" + bitValue); 129 | value += bitValue; 130 | } 131 | return value; 132 | } 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/wzh/p2ptest/RtcActivity.java: -------------------------------------------------------------------------------- 1 | package com.wzh.p2ptest; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.os.Bundle; 6 | import android.os.Looper; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.text.TextUtils; 9 | import android.util.Log; 10 | import android.view.View; 11 | import android.widget.Button; 12 | import android.widget.EditText; 13 | import android.widget.ScrollView; 14 | import android.widget.TextView; 15 | 16 | import com.wzh.p2ptest.WebRtcClient.IMessageReceiver; 17 | 18 | import org.json.JSONException; 19 | import org.webrtc.DataChannel; 20 | import org.webrtc.VideoRendererGui; 21 | 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.FileNotFoundException; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | 27 | //发送请求端 28 | public class RtcActivity extends AppCompatActivity implements WebRtcClient.RtcListener { 29 | 30 | private WebRtcClient client; 31 | private String mSocketAddress; 32 | private String callerId; 33 | 34 | public static final String TAG = "RtcActivity"; 35 | private TextView mTvContent; 36 | private Button mBtConnect; 37 | private EditText mEditText; 38 | private DataChannel dataChannel; 39 | private ScrollView mScrollView; 40 | 41 | @Override 42 | public void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | 45 | setContentView(R.layout.main); 46 | mSocketAddress = "http://" + getResources().getString(R.string.host); 47 | mSocketAddress += (":" + getResources().getString(R.string.port) + "/"); 48 | 49 | callerId = "";//先用一台设备运行,然后通过日志获取id,填入id,再运行到另一台设备上 50 | initView(); 51 | init(); 52 | } 53 | 54 | private void initView() { 55 | mTvContent = (TextView) findViewById(R.id.tv_content); 56 | mBtConnect = (Button) findViewById(R.id.bt_connect); 57 | mEditText = (EditText) findViewById(R.id.ed_text); 58 | mScrollView = (ScrollView) findViewById(R.id.scrollView); 59 | 60 | appText("服务器:" + mSocketAddress); 61 | 62 | } 63 | 64 | private void init() { 65 | client = new WebRtcClient(this, mSocketAddress, VideoRendererGui.getEGLContext()); 66 | 67 | client.setIMessageReceiver(new IMessageReceiver() { 68 | @Override 69 | public void onReceiverStart() { 70 | appText("开始接收"); 71 | } 72 | 73 | @Override 74 | public void onReceiverProcess(float process) { 75 | appText("接收中" + process); 76 | } 77 | 78 | @Override 79 | public void onReceiverSuccess(byte[] data, int type) { 80 | appText("接收完成" + data.length); 81 | 82 | if (type == 1) {//text 83 | appText("收到 " + new String(data)); 84 | } else if (type == 2) {//bitmap 85 | Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); 86 | appText("收到 bitmap" + bitmap.getWidth() + " *" + bitmap.getHeight() + " =" + bitmap.getByteCount()); 87 | Utils.safeShowBitmapDialog(RtcActivity.this, bitmap); 88 | } else if (type == 3) {//文件 89 | //写入文件。。。 90 | Log.d(TAG, "file size=" + data.length); 91 | appText("收到 文件 fileSize"+data.length); 92 | } 93 | } 94 | 95 | }); 96 | } 97 | 98 | public void onClick(View view) throws FileNotFoundException { 99 | if (dataChannel == null) { 100 | appText("p2p连接未建立"); 101 | return; 102 | } else if (dataChannel.state() != DataChannel.State.OPEN) { 103 | appText("p2p连接未打开"); 104 | return; 105 | } 106 | switch (view.getId()) { 107 | case R.id.bt_connect: 108 | String text = mEditText.getText().toString().trim(); 109 | if (TextUtils.isEmpty(text)) 110 | return; 111 | client.sendData(dataChannel, text.getBytes(), 1); 112 | appText(text + " 已发送"); 113 | break; 114 | case R.id.bt_sendimg: 115 | new Thread(new Runnable() { 116 | @Override 117 | public void run() { 118 | 119 | try { 120 | 121 | Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.aaa); 122 | Log.d(TAG, "发送" + bitmap.getWidth() + "*" + bitmap.getHeight() + "=" + bitmap.getByteCount()); 123 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 124 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos); 125 | byte[] arrays = bos.toByteArray();//362296 126 | bos.close(); 127 | 128 | client.sendData(dataChannel, arrays, 2); 129 | 130 | appText("bitmap 已发送 " + arrays.length); 131 | 132 | } catch (IOException e) { 133 | e.printStackTrace(); 134 | } 135 | } 136 | }).start(); 137 | 138 | break; 139 | case R.id.bt_sendfile: 140 | new Thread(new Runnable() { 141 | @Override 142 | public void run() { 143 | try { 144 | InputStream stream = getAssets().open("music.mp3"); 145 | Log.d(TAG, "send file size=" + stream.available()); 146 | byte[] data = new byte[stream.available()]; 147 | stream.read(data); 148 | client.sendData(dataChannel, data, 3); 149 | appText("文件已发送 send file size=" + stream.available()); 150 | } catch (Exception e) { 151 | e.printStackTrace(); 152 | } 153 | 154 | } 155 | }).start(); 156 | 157 | 158 | break; 159 | default: 160 | 161 | break; 162 | } 163 | } 164 | 165 | @Override 166 | public void onPause() { 167 | super.onPause(); 168 | if (client != null) { 169 | client.onPause(); 170 | } 171 | } 172 | 173 | @Override 174 | public void onResume() { 175 | super.onResume(); 176 | if (client != null) { 177 | client.onResume(); 178 | } 179 | } 180 | 181 | @Override 182 | public void onDestroy() { 183 | if (client != null) { 184 | client.onDestroy(); 185 | } 186 | super.onDestroy(); 187 | } 188 | 189 | @Override 190 | public void onCallReady(String callId) { 191 | 192 | String streamName = "androidP2pControl"; 193 | appText("我的id 为:" + callId + " stream name=" + streamName + " 等待连接"); 194 | client.start(streamName); 195 | 196 | if (callerId != null) { 197 | try { 198 | answer(callerId); 199 | } catch (JSONException e) { 200 | e.printStackTrace(); 201 | } 202 | } 203 | 204 | } 205 | 206 | private void appText(final String msg) { 207 | if (Looper.myLooper() != Looper.getMainLooper()) { 208 | runOnUiThread(new Runnable() { 209 | @Override 210 | public void run() { 211 | mTvContent.append(msg + "\n"); 212 | 213 | mScrollView.postDelayed(new Runnable() { 214 | @Override 215 | public void run() { 216 | mScrollView.fullScroll(ScrollView.FOCUS_DOWN); 217 | } 218 | }, 100); 219 | } 220 | }); 221 | } else { 222 | mTvContent.append(msg + "\n"); 223 | } 224 | } 225 | 226 | public void answer(String callerId) throws JSONException { 227 | client.sendMessage(callerId, "init", null); 228 | appText("开始连接" + callerId); 229 | 230 | } 231 | 232 | @Override 233 | public void onStatusChanged(final String newStatus) { 234 | runOnUiThread(new Runnable() { 235 | @Override 236 | public void run() { 237 | // Toast.makeText(getApplicationContext(), newStatus, Toast.LENGTH_SHORT).show(); 238 | appText(newStatus); 239 | } 240 | }); 241 | } 242 | 243 | @Override 244 | public void onDataChannel(DataChannel dataChannel) { 245 | this.dataChannel = dataChannel; 246 | } 247 | 248 | } -------------------------------------------------------------------------------- /app/src/main/java/com/wzh/p2ptest/ScreenActivity.java: -------------------------------------------------------------------------------- 1 | package com.wzh.p2ptest; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.hardware.display.DisplayManager; 6 | import android.hardware.display.VirtualDisplay; 7 | import android.media.MediaRecorder; 8 | import android.media.projection.MediaProjection; 9 | import android.media.projection.MediaProjectionManager; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.os.Environment; 13 | import android.support.annotation.Nullable; 14 | import android.support.annotation.RequiresApi; 15 | import android.support.v7.app.AppCompatActivity; 16 | import android.util.DisplayMetrics; 17 | import android.util.Log; 18 | import android.view.View; 19 | 20 | import java.io.File; 21 | import java.io.IOException; 22 | 23 | /** 24 | * Created by wzh on 06/08/2018. 25 | */ 26 | 27 | public class ScreenActivity extends AppCompatActivity{ 28 | 29 | private MediaProjectionManager manager; 30 | private MediaProjection mediaProject; 31 | private DisplayMetrics metrics; 32 | private MediaRecorder mediaRecorder; 33 | private String TAG="ScreenActivity"; 34 | private boolean running; 35 | 36 | @Override 37 | protected void onCreate(@Nullable Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_screen); 40 | 41 | initView(); 42 | 43 | metrics=getResources().getDisplayMetrics(); 44 | } 45 | 46 | private void initView() { 47 | 48 | manager= (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); 49 | Intent intent= null; 50 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { 51 | intent = manager.createScreenCaptureIntent(); 52 | } 53 | startActivityForResult(intent,10); 54 | } 55 | 56 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 57 | public void onClick(View view){ 58 | switch(view.getId()){ 59 | case R.id.button: 60 | startRecord(); 61 | break; 62 | case R.id.button1: 63 | mediaRecorder.stop(); 64 | mediaProject.stop(); 65 | break; 66 | default: 67 | 68 | break; 69 | } 70 | } 71 | 72 | private void initRecorder() { 73 | mediaRecorder =new MediaRecorder(); 74 | 75 | File file = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".mp4"); 76 | mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); 77 | mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); 78 | mediaRecorder.setOutputFile(file.getAbsolutePath()); 79 | mediaRecorder.setVideoSize(metrics.widthPixels, metrics.heightPixels); 80 | mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); 81 | mediaRecorder.setVideoEncodingBitRate(5 * 1024 * 1024); 82 | mediaRecorder.setVideoFrameRate(30); 83 | try { 84 | mediaRecorder.prepare(); 85 | } catch (IOException e) { 86 | e.printStackTrace(); 87 | } 88 | } 89 | 90 | public boolean startRecord() { 91 | if (mediaProject == null || running) { 92 | return false; 93 | } 94 | initRecorder(); 95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 96 | createVirtualDisplay(); 97 | } 98 | mediaRecorder.start(); 99 | running = true; 100 | return true; 101 | } 102 | 103 | 104 | @Override 105 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 106 | super.onActivityResult(requestCode, resultCode, data); 107 | if(requestCode==10){ 108 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 109 | mediaProject=manager.getMediaProjection(resultCode,data); 110 | } 111 | } 112 | } 113 | 114 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 115 | private void createVirtualDisplay() { 116 | mediaProject.createVirtualDisplay("mainScreen",metrics.widthPixels,metrics.heightPixels,metrics.densityDpi, 117 | DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mediaRecorder.getSurface(),new VirtualDisplay.Callback() { 118 | 119 | @Override 120 | public void onPaused() { 121 | super.onPaused(); 122 | Log.d(TAG,"onPaused"); 123 | 124 | } 125 | 126 | @Override 127 | public void onResumed() { 128 | super.onResumed(); 129 | Log.d(TAG,"onResumed"); 130 | 131 | } 132 | 133 | @Override 134 | public void onStopped() { 135 | super.onStopped(); 136 | Log.d(TAG,"onStopped"); 137 | } 138 | },null); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/src/main/java/com/wzh/p2ptest/Utils.java: -------------------------------------------------------------------------------- 1 | package com.wzh.p2ptest; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Bitmap; 5 | import android.graphics.Color; 6 | import android.os.Looper; 7 | import android.support.v7.app.AlertDialog; 8 | import android.view.Gravity; 9 | import android.widget.ImageView; 10 | import android.widget.LinearLayout; 11 | 12 | /** 13 | * Created by wzh on 02/08/2018. 14 | */ 15 | 16 | public class Utils { 17 | 18 | 19 | public static void safeShowBitmapDialog(final Activity activity,final Bitmap bitmap) { 20 | if (Looper.getMainLooper() != Looper.myLooper()) { 21 | activity.runOnUiThread(new Runnable() { 22 | @Override 23 | public void run() { 24 | showBitmapDialog(activity,bitmap); 25 | } 26 | }); 27 | } else { 28 | showBitmapDialog(activity,bitmap); 29 | } 30 | 31 | } 32 | 33 | public static void showBitmapDialog(Activity activity,final Bitmap bitmap) { 34 | Activity act = activity; 35 | LinearLayout ll = new LinearLayout(act); 36 | ll.setBackgroundColor(Color.BLACK); 37 | ll.setGravity(Gravity.CENTER); 38 | 39 | ImageView view = new ImageView(act); 40 | view.setImageBitmap(bitmap); 41 | ll.addView(view); 42 | new AlertDialog.Builder(act).setView(ll).show(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/wzh/p2ptest/WebRtcClient.java: -------------------------------------------------------------------------------- 1 | package com.wzh.p2ptest; 2 | 3 | import android.opengl.EGLContext; 4 | import android.support.annotation.NonNull; 5 | import android.util.Log; 6 | 7 | import com.github.nkzawa.emitter.Emitter; 8 | import com.github.nkzawa.socketio.client.IO; 9 | import com.github.nkzawa.socketio.client.Socket; 10 | 11 | import org.json.JSONException; 12 | import org.json.JSONObject; 13 | import org.webrtc.DataChannel; 14 | import org.webrtc.IceCandidate; 15 | import org.webrtc.MediaConstraints; 16 | import org.webrtc.MediaStream; 17 | import org.webrtc.PeerConnection; 18 | import org.webrtc.PeerConnectionFactory; 19 | import org.webrtc.SdpObserver; 20 | import org.webrtc.SessionDescription; 21 | 22 | import java.net.URISyntaxException; 23 | import java.nio.ByteBuffer; 24 | import java.util.HashMap; 25 | import java.util.LinkedList; 26 | 27 | public class WebRtcClient { 28 | private final static String TAG = WebRtcClient.class.getCanonicalName(); 29 | private final static int MAX_PEER = 2; 30 | private boolean[] endPoints = new boolean[MAX_PEER]; 31 | private PeerConnectionFactory factory; 32 | private HashMap peers = new HashMap<>(); 33 | private LinkedList iceServers = new LinkedList<>(); 34 | private MediaConstraints pcConstraints = new MediaConstraints(); 35 | private RtcListener mListener; 36 | private Socket client; 37 | 38 | private IMessageReceiver iMessageReceiver; 39 | 40 | /** 41 | * Implement this interface to be notified of events. 42 | */ 43 | public interface RtcListener { 44 | void onCallReady(String callId); 45 | 46 | void onStatusChanged(String newStatus); 47 | void onDataChannel(DataChannel dataChannel); 48 | 49 | } 50 | 51 | public IMessageReceiver getIMessageReceiver() { 52 | return iMessageReceiver; 53 | } 54 | 55 | public void setIMessageReceiver(IMessageReceiver iMessageReceiver) { 56 | this.iMessageReceiver = iMessageReceiver; 57 | } 58 | 59 | private interface Command { 60 | void execute(String peerId, JSONObject payload) throws JSONException; 61 | } 62 | 63 | private class CreateOfferCommand implements Command { 64 | public void execute(String peerId, JSONObject payload) throws JSONException { 65 | Log.d(TAG, "CreateOfferCommand peerId=" + peerId + " payload=" + payload); 66 | Peer peer = peers.get(peerId); 67 | peer.pc.createOffer(peer, pcConstraints); 68 | } 69 | } 70 | 71 | private class CreateAnswerCommand implements Command { 72 | public void execute(String peerId, JSONObject payload) throws JSONException { 73 | Log.d(TAG, "CreateAnswerCommand peerId=" + peerId + " payload=" + payload); 74 | Peer peer = peers.get(peerId); 75 | SessionDescription sdp = new SessionDescription( 76 | SessionDescription.Type.fromCanonicalForm(payload.getString("type")), 77 | payload.getString("sdp") 78 | ); 79 | peer.pc.setRemoteDescription(peer, sdp); 80 | peer.pc.createAnswer(peer, pcConstraints); 81 | } 82 | } 83 | 84 | private class SetRemoteSDPCommand implements Command { 85 | public void execute(String peerId, JSONObject payload) throws JSONException { 86 | Log.d(TAG, "SetRemoteSDPCommand peerId=" + peerId + " payload=" + payload); 87 | Peer peer = peers.get(peerId); 88 | SessionDescription sdp = new SessionDescription( 89 | SessionDescription.Type.fromCanonicalForm(payload.getString("type")), 90 | payload.getString("sdp") 91 | ); 92 | peer.pc.setRemoteDescription(peer, sdp); 93 | } 94 | } 95 | 96 | private class AddIceCandidateCommand implements Command { 97 | public void execute(String peerId, JSONObject payload) throws JSONException { 98 | // Log.d(TAG,"AddIceCandidateCommand peerId="+peerId+" payload="+payload); 99 | PeerConnection pc = peers.get(peerId).pc; 100 | if (pc.getRemoteDescription() != null) { 101 | IceCandidate candidate = new IceCandidate( 102 | payload.getString("id"), 103 | payload.getInt("label"), 104 | payload.getString("candidate") 105 | ); 106 | pc.addIceCandidate(candidate); 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Send a message through the signaling server 113 | * 114 | * @param to id of recipient 115 | * @param type type of message 116 | * @param payload payload of message 117 | * @throws JSONException 118 | */ 119 | public void sendMessage(String to, String type, JSONObject payload) throws JSONException { 120 | JSONObject message = new JSONObject(); 121 | message.put("to", to); 122 | message.put("type", type); 123 | message.put("payload", payload); 124 | client.emit("message", message); 125 | Log.i(TAG, "sendMessage=" + message); 126 | } 127 | 128 | private class MessageHandler { 129 | private HashMap commandMap; 130 | 131 | private MessageHandler() { 132 | this.commandMap = new HashMap<>(); 133 | commandMap.put("init", new CreateOfferCommand()); 134 | commandMap.put("offer", new CreateAnswerCommand()); 135 | commandMap.put("answer", new SetRemoteSDPCommand()); 136 | commandMap.put("candidate", new AddIceCandidateCommand()); 137 | } 138 | 139 | private Emitter.Listener onMessage = new Emitter.Listener() { 140 | @Override 141 | public void call(Object... args) { 142 | JSONObject data = (JSONObject) args[0]; 143 | try { 144 | String from = data.getString("from"); 145 | String type = data.getString("type"); 146 | JSONObject payload = null; 147 | if (!type.equals("init")) { 148 | payload = data.getJSONObject("payload"); 149 | } 150 | Log.d(TAG, "onMessage data=" + data); 151 | // if peer is unknown, try to add him 152 | if (!peers.containsKey(from)) { 153 | // if MAX_PEER is reach, ignore the call 154 | int endPoint = findEndPoint(); 155 | if (endPoint != MAX_PEER) { 156 | Peer peer = addPeer(from, endPoint); 157 | commandMap.get(type).execute(from, payload); 158 | } 159 | } else { 160 | commandMap.get(type).execute(from, payload); 161 | } 162 | } catch (JSONException e) { 163 | e.printStackTrace(); 164 | } 165 | } 166 | }; 167 | 168 | private Emitter.Listener onId = new Emitter.Listener() { 169 | @Override 170 | public void call(Object... args) { 171 | String id = (String) args[0]; 172 | Log.d(TAG, "onId data=" + id); 173 | 174 | mListener.onCallReady(id); 175 | } 176 | }; 177 | } 178 | 179 | private class Peer implements SdpObserver, PeerConnection.Observer { 180 | private DataChannel channel; 181 | private PeerConnection pc; 182 | private String id; 183 | private int endPoint; 184 | 185 | @Override 186 | public void onCreateSuccess(final SessionDescription sdp) { 187 | // TODO: modify sdp to use pcParams prefered codecs 188 | try { 189 | JSONObject payload = new JSONObject(); 190 | payload.put("type", sdp.type.canonicalForm()); 191 | payload.put("sdp", sdp.description); 192 | Log.i(TAG, "onCreateSuccess" + sdp); 193 | sendMessage(id, sdp.type.canonicalForm(), payload); 194 | pc.setLocalDescription(Peer.this, sdp); 195 | } catch (JSONException e) { 196 | e.printStackTrace(); 197 | } 198 | } 199 | 200 | @Override 201 | public void onSetSuccess() { 202 | Log.i(TAG, "Peer onSetSuccess"); 203 | 204 | } 205 | 206 | @Override 207 | public void onCreateFailure(String s) { 208 | Log.i(TAG, "Peer onCreateFailure " + s); 209 | 210 | } 211 | 212 | @Override 213 | public void onSetFailure(String s) { 214 | Log.i(TAG, "Peer onSetFailure " + s); 215 | 216 | } 217 | 218 | @Override 219 | public void onSignalingChange(PeerConnection.SignalingState signalingState) { 220 | Log.i(TAG, "Peer onSignalingChange " + signalingState); 221 | mListener.onStatusChanged("onSignalingChange " + signalingState.name()); 222 | 223 | } 224 | 225 | @Override 226 | public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { 227 | mListener.onStatusChanged("onIceConnectionChange " + iceConnectionState.name()); 228 | 229 | if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) { 230 | removePeer(id); 231 | } else if (iceConnectionState == PeerConnection.IceConnectionState.CONNECTED) {//连接成功 232 | mListener.onStatusChanged("连接成功"); 233 | } 234 | Log.i(TAG, "Peer onIceConnectionChange " + iceConnectionState); 235 | } 236 | 237 | @Override 238 | public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { 239 | Log.i(TAG, "Peer onIceGatheringChange " + iceGatheringState); 240 | mListener.onStatusChanged("onIceGatheringChange " + iceGatheringState.name()); 241 | 242 | 243 | } 244 | 245 | @Override 246 | public void onIceCandidate(final IceCandidate candidate) { 247 | try { 248 | JSONObject payload = new JSONObject(); 249 | payload.put("label", candidate.sdpMLineIndex); 250 | payload.put("id", candidate.sdpMid); 251 | payload.put("candidate", candidate.sdp); 252 | sendMessage(id, "candidate", payload); 253 | 254 | // Log.i(TAG,"onIceCandidate "+candidate); 255 | 256 | } catch (JSONException e) { 257 | e.printStackTrace(); 258 | } 259 | } 260 | 261 | @Override 262 | public void onAddStream(MediaStream mediaStream) { 263 | Log.d(TAG, "onAddStream " + mediaStream.label()); 264 | // remote streams are displayed from 1 to MAX_PEER (0 is localStream) 265 | } 266 | 267 | @Override 268 | public void onRemoveStream(MediaStream mediaStream) { 269 | Log.d(TAG, "onRemoveStream " + mediaStream.label()); 270 | removePeer(id); 271 | } 272 | 273 | @Override 274 | public void onDataChannel(final DataChannel dataChannel) { 275 | Log.i(TAG, "Peer onDataChannel=" + dataChannel); 276 | channel=dataChannel; 277 | mListener.onDataChannel(dataChannel); 278 | mListener.onStatusChanged("DataChannel " + channel.state()); 279 | 280 | } 281 | 282 | @Override 283 | public void onRenegotiationNeeded() { 284 | Log.i(TAG, "Peer onRenegotiationNeeded="); 285 | 286 | } 287 | 288 | public Peer(String id, int endPoint) { 289 | Log.d(TAG, "new Peer: " + id + " " + endPoint); 290 | this.pc = factory.createPeerConnection(iceServers, pcConstraints, this); 291 | // pc.createDataChannel() 292 | this.id = id; 293 | this.endPoint = endPoint; 294 | 295 | // pc.addStream(localMS); //, new MediaConstraints() 296 | 297 | mListener.onStatusChanged("CONNECTING"); 298 | 299 | channel = pc.createDataChannel("sendDataChannel", new DataChannel.Init()); 300 | Log.i(TAG, "createDataChannel " + channel); 301 | channel.registerObserver(new DataChannel.Observer() { 302 | 303 | byte[] mReceiveSrcData; 304 | 305 | @Override 306 | public void onStateChange() { 307 | Log.d(TAG, "registerObserver onStateChange" + channel.state()); 308 | mListener.onStatusChanged("DataChannel " + channel.state()); 309 | } 310 | 311 | @Override 312 | public void onMessage(DataChannel.Buffer buffer) { 313 | Log.d(TAG, "registerObserver onMessage" + buffer + " data=" + buffer.data + " isBinary=" + buffer.binary); 314 | 315 | int capacity = buffer.data.capacity(); 316 | byte[] bytes = new byte[capacity]; 317 | buffer.data.get(bytes); 318 | 319 | int index = (int) getValue(bytes, 0, 2); 320 | long packSize = getValue(bytes, 2, 4);//总包数 321 | long packType = getValue(bytes, 4, 5);//包所在类型 322 | int totalDataLength = (int) getValue(bytes, 5, 10);//总数据长度 323 | int dataLength = (int) getValue(bytes, 10, 13);//当前包data长度 324 | int type = (int) getValue(bytes, 13, 14);//type 325 | 326 | if (index == 0) 327 | mReceiveSrcData = new byte[totalDataLength]; 328 | 329 | System.arraycopy(bytes, PACK_DATA_START_INDEX, mReceiveSrcData, index * MAX_PACK_DATA_SIZE, dataLength); 330 | 331 | if (iMessageReceiver != null) { 332 | if (packType == 0 || index==0) { 333 | iMessageReceiver.onReceiverStart(); 334 | } 335 | if (packType == 1) { 336 | iMessageReceiver.onReceiverProcess((float) ((double) index*100 / packSize)); 337 | } 338 | if (packType == 2) { 339 | iMessageReceiver.onReceiverSuccess(mReceiveSrcData,type); 340 | mReceiveSrcData =null; 341 | } 342 | } 343 | 344 | } 345 | }); 346 | 347 | 348 | } 349 | } 350 | 351 | private Peer addPeer(String id, int endPoint) { 352 | Peer peer = new Peer(id, endPoint); 353 | peers.put(id, peer); 354 | 355 | endPoints[endPoint] = true; 356 | return peer; 357 | } 358 | 359 | private void removePeer(String id) { 360 | Peer peer = peers.get(id); 361 | peer.pc.close(); 362 | peers.remove(peer.id); 363 | endPoints[peer.endPoint] = false; 364 | } 365 | 366 | public WebRtcClient(RtcListener listener, String host, EGLContext mEGLcontext) { 367 | mListener = listener; 368 | PeerConnectionFactory.initializeAndroidGlobals(listener, true, true, 369 | true, mEGLcontext); 370 | factory = new PeerConnectionFactory(); 371 | MessageHandler messageHandler = new MessageHandler(); 372 | 373 | try { 374 | client = IO.socket(host); 375 | } catch (URISyntaxException e) { 376 | e.printStackTrace(); 377 | } 378 | client.on("id", messageHandler.onId); 379 | client.on("message", messageHandler.onMessage); 380 | client.connect(); 381 | iceServers.add(new PeerConnection.IceServer("stun:39.104.185.143:3478")); 382 | 383 | // stun 打洞服务器 turn 打洞不行后中继服务服务器 384 | iceServers.add(new PeerConnection.IceServer("turn:39.104.185.143:3478?transport=udp", "webrtc", "webrtc")); 385 | iceServers.add(new PeerConnection.IceServer("turn:39.104.185.143:3478?transport=tcp", "webrtc", "webrtc")); 386 | 387 | /* 388 | iceServers.add(new PeerConnection.IceServer("stun:23.21.150.121")); 389 | iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302")); 390 | iceServers.add(new PeerConnection.IceServer("stun:stun.services.mozilla.com")); 391 | 392 | //免费的turn服务 393 | iceServers.add(new PeerConnection.IceServer("turn:numb.viagenie.ca", "", ""));//这个网站可以自己去申请账号密码 394 | iceServers.add(new PeerConnection.IceServer("turn:192.158.29.39:3478?transport=udp", "28224511:1379330808", "JZEOEt2V3Qb0y27GRntt2u2PAYA=")); 395 | iceServers.add(new PeerConnection.IceServer("turn:192.158.29.39:3478?transport=tcp", "28224511:1379330808", "JZEOEt2V3Qb0y27GRntt2u2PAYA=")); 396 | 397 | iceServers.add(new PeerConnection.IceServer("turn:turn.bistri.com:80", "homeo", "homeo")); 398 | iceServers.add(new PeerConnection.IceServer("turn:turn.anyfirewall.com:443?transport=tcp", "webrtc", "webrtc")); 399 | 400 | */ 401 | pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")); 402 | pcConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")); 403 | 404 | pcConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); 405 | } 406 | 407 | /** 408 | * Call this method in Activity.onPause() 409 | */ 410 | public void onPause() { 411 | } 412 | 413 | /** 414 | * Call this method in Activity.onResume() 415 | */ 416 | public void onResume() { 417 | } 418 | 419 | /** 420 | * Call this method in Activity.onDestroy() 421 | */ 422 | public void onDestroy() { 423 | for (Peer peer : peers.values()) { 424 | peer.pc.dispose(); 425 | } 426 | factory.dispose(); 427 | client.disconnect(); 428 | client.close(); 429 | } 430 | 431 | private int findEndPoint() { 432 | for (int i = 0; i < MAX_PEER; i++) if (!endPoints[i]) return i; 433 | return MAX_PEER; 434 | } 435 | 436 | /** 437 | * Start the client. 438 | *

439 | * Set up the local stream and notify the signaling server. 440 | * Call this method after onCallReady. 441 | * 442 | * @param name client name 443 | */ 444 | public void start(String name) { 445 | try { 446 | JSONObject message = new JSONObject(); 447 | message.put("name", name); 448 | client.emit("readyToStream", message); 449 | } catch (JSONException e) { 450 | e.printStackTrace(); 451 | } 452 | } 453 | 454 | public static final int MAX_PACK_SIZE = 66528;//每个包最大size 455 | public static final int MAX_PACK_DATA_SIZE = 65536;//每个包中数据体最大size 456 | public static final int PACK_DATA_START_INDEX = MAX_PACK_SIZE - MAX_PACK_DATA_SIZE;//包中data体前剩余空间,用于存放索引、总长度等 457 | 458 | //2^14-1=16383 459 | public static final int MAX_INDEX = (int) (Math.pow(2, 14) - 1); 460 | 461 | /** 462 | * 发送数据 463 | * @param channel 和peer 形成的数据通道 464 | * @param data 数据 465 | * @param type 数据类型,自定义 466 | * @return 467 | */ 468 | public boolean sendData(DataChannel channel, @NonNull byte[] data,int type) { 469 | DataChannel.State state = null; 470 | if (channel == null || (state = channel.state()) != DataChannel.State.OPEN) { 471 | Log.d(TAG, "channel = " + channel + " and state is " + state); 472 | return false; 473 | } 474 | int len = data.length; 475 | if (len > 1073676288) { 476 | throw new RuntimeException("data.length range 1 ~ 16383 *$MAX_PACK_DATA_SIZE"); 477 | } 478 | int quotient = len / MAX_PACK_DATA_SIZE;//商 479 | int remainder = len % MAX_PACK_DATA_SIZE;//余数 480 | 481 | int packSize = (remainder == 0 ? quotient : quotient + 1);//总包数 482 | 483 | Log.d(TAG, "arrays.len=" + len + " quotient=" + quotient + " remainder=" + remainder); 484 | for (int i = 0; i < quotient; i++) { 485 | byte[] dest = new byte[MAX_PACK_SIZE]; 486 | int start = i * MAX_PACK_DATA_SIZE; 487 | System.arraycopy(data, start, dest, PACK_DATA_START_INDEX, MAX_PACK_DATA_SIZE); 488 | setValue(dest, 0, 2, i);//index 489 | setValue(dest, 2, 4, packSize);//总包数 490 | setValue(dest, 4, 5, buildType(quotient, remainder, i));//包所在类型 491 | setValue(dest, 5, 10, len);//总数据长度 492 | setValue(dest, 10, 13, MAX_PACK_DATA_SIZE);//当前包data长度 493 | setValue(dest, 13, 14, type);//自定义type 494 | 495 | // Log.d(TAG, "i" + i + " start " + start + " length=" + dest.length); 496 | channel.send(new DataChannel.Buffer(ByteBuffer.wrap(dest), true)); 497 | } 498 | 499 | if (remainder != 0) {//不够packSize 的end 500 | 501 | byte[] dest = new byte[remainder + PACK_DATA_START_INDEX]; 502 | System.arraycopy(data, len - remainder, dest, PACK_DATA_START_INDEX, remainder); 503 | 504 | setValue(dest, 0, 2, packSize-1);//index 505 | setValue(dest, 2, 4, packSize);//总包数 506 | setValue(dest, 4, 5, 2);//包所在类型 507 | setValue(dest, 5, 10, len);//总数据长度 508 | setValue(dest, 10, 13, remainder);//当前包data长度 509 | setValue(dest, 13, 14, type);//自定义type 510 | 511 | // Log.d(TAG, "remainder=" + remainder); 512 | channel.send(new DataChannel.Buffer(ByteBuffer.wrap(dest), true)); 513 | } 514 | return true; 515 | } 516 | 517 | /** 518 | * 把long 型 value 值 转换成byte数组 表示 519 | * 如 setValue(data,0,2,333) 表示在数组中0,1两个byte 中分隔存放 333 520 | * 521 | * @param data 522 | * @param firstPosition 数据存放开始索引 523 | * @param endPosition 数据存放结束索引位置,在数组不包含此位置,如 firstPosition=0,endPosition=2,表示 0,1索引 524 | * @param value 要表示的int值 525 | */ 526 | private static void setValue(byte[] data, int firstPosition, int endPosition, long value) { 527 | if (endPosition - firstPosition < 1) 528 | throw new IndexOutOfBoundsException("end - first must > 0 " + endPosition + " - " + firstPosition); 529 | 530 | if (value <= Byte.MAX_VALUE) {//byte -128~127 531 | for (int i = firstPosition; i < endPosition - 1; i++) { 532 | data[i] = 0; 533 | } 534 | data[endPosition - 1] = (byte) value; 535 | } else { 536 | String binary = Long.toBinaryString(value);//二进制 537 | int len = binary.length(); 538 | int maxBit = (endPosition - firstPosition) * 7; 539 | if (len > maxBit) {//超出最大可表示数 540 | throw new IndexOutOfBoundsException(value + "--> " + binary + " out of " + maxBit + " bit byte value"); 541 | } 542 | 543 | for (int i = endPosition - 1; i >= firstPosition; i--) { 544 | long newValue = value >> 7;//去除后七位 545 | data[i] = (byte) (value - (newValue << 7));//加上七位0,相减就是后七位值 546 | value = newValue; 547 | } 548 | } 549 | } 550 | 551 | /** 552 | * 把fir~end 位的数转换为对应value 553 | * 554 | * @param data 555 | * @param firstPosition 556 | * @param endPosition 如fir=0,end=2,表示0,1两位,不包含索引2 557 | * @return 558 | */ 559 | private static long getValue(byte[] data, int firstPosition, int endPosition) { 560 | if (endPosition - firstPosition < 1) 561 | throw new IndexOutOfBoundsException("end - first must > 0 " + endPosition + " - " + firstPosition); 562 | 563 | long value = 0; 564 | int bit = endPosition - firstPosition - 1; 565 | for (int i = firstPosition; i < endPosition; i++, bit--) { 566 | long bitValue = (long) data[i] << (7 * bit); 567 | // System.out.println("bit: " + bit + " bitV=" + bitValue); 568 | value += bitValue; 569 | } 570 | return value; 571 | } 572 | 573 | private int buildType(int size, int footer, int i) { 574 | int type; 575 | if (i == 0) 576 | type = 0;//头 577 | else if (footer == 0) { 578 | if (i != size - 1) 579 | type = 1;//中间部分 580 | else 581 | type = 2;//尾 582 | } else { 583 | type = 1; 584 | } 585 | return type; 586 | } 587 | 588 | public interface IMessageReceiver { 589 | void onReceiverStart(); 590 | 591 | void onReceiverProcess(float process); 592 | 593 | void onReceiverSuccess(byte[] data,int type); 594 | } 595 | 596 | } 597 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/aaa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zihao-Wu/AndroidP2pTest/53ac2be5315096ffc4e803c2b2aa892df3a85243/app/src/main/res/drawable/aaa.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_screen.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 |