【Android】螢屏共享與反向控制功能的實作
- 前言
- 一、功能介紹
- 1.螢屏共享
- 2.反向控制
- 二、功能原理
- 1.原理框圖
- 2.作業原理
- (1)MediaProjection截屏
- (2)SurfaceView顯示
- (3)TCP傳輸Bitmap
- (4)ADB埠轉發
- 三、效果演示
- 總結
前言
之前用了一下QQ電腦版的遠程協助,發現這個功能很方便實用,于是就想開發一款類似功能的APP,無奈本人只會一點點Android和Java,開發程序中爬了很多坑,但是經過不懈努力,終于把基本功能實作了,
一、功能介紹
1.螢屏共享
這個APP主要有螢屏共享和反向控制兩個功能,螢屏共享功能的實作需要兩臺手機,一臺手機作為服務端,共享螢屏;另一臺手機做客戶端,顯示螢屏,服務端與客戶端需要在同一局域網或熱點連接,服務端主要是通過MediaProjection實時截屏,通過TCP把圖片資料發送給客戶端;客戶端則把TCP接收的圖片資料通過SurfaceView渲染顯示,
2.反向控制
反向控制的功能主要是結合了ADB,這個功能的實作需要手機服務端先開啟 開發者模式及USB除錯,然后用USB連接電腦端,共享螢屏時,在電腦端運行Python或其他語言撰寫的腳本,客戶端的SurfaceView會偵聽用戶的觸摸事件,并通過服務端TCP傳輸給電腦端,電腦端則發送ADB命令給服務端,從而實作客戶端反向控制服務端的功能,
二、功能原理
1.原理框圖

2.作業原理
(1)MediaProjection截屏
MediaProjection是Google在Android5.0之后給開發者提供的截屏或錄屏方法,在使用MediaProjection之前需要先申請權限,
private void Request_Media_Projection_Permission() {
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) this.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent intent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(intent, REQUEST_MEDIA_PROJECTION_CODE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_MEDIA_PROJECTION_CODE) {
if (resultCode != Activity.RESULT_OK) {
Toast.makeText(this, "Media Projection Permission Denied", Toast.LENGTH_SHORT).show();
return;
}
MyUtils.setResultCode(resultCode);
MyUtils.setResultData(data);
}
}
private ScreenCapture(Context context, int resultCode, Intent data) {
MediaProjectionManager mMediaProjectionManager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
screen_width = MyUtils.getScreenWidth();
screen_height = MyUtils.getScreenHeight();
screen_density = MyUtils.getScreenDensity();
mImageReader = ImageReader.newInstance(
screen_width,
screen_height,
PixelFormat.RGBA_8888,
2);
}
public static ScreenCapture getInstance(Context context, int resultCode, Intent data) {
if(screenCapture == null) {
synchronized (ScreenCapture.class) {
if(screenCapture == null) {
screenCapture = new ScreenCapture(context, resultCode, data);
}
}
}
return screenCapture;
}
MediaProjection通過createVirtualDisplay來截屏,我們可以通過ImageReader的setOnImageAvailableListener把截屏資料轉為Bitmap資料,
private void setUpVirtualDisplay() {
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
"ScreenCapture",
screen_width,
screen_height,
screen_density,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mImageReader.getSurface(),
null,
null);
mImageReader.setOnImageAvailableListener(this, null);
}
@Override
public void onImageAvailable(ImageReader imageReader) {
try {
Image image = imageReader.acquireLatestImage();
if(image != null) {
Image.Plane[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * screen_width;
Bitmap bitmap = Bitmap.createBitmap(screen_width + rowPadding / pixelStride, screen_height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
MyUtils.setBitmap(bitmap);
image.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
(2)SurfaceView顯示
SurfaceView渲染圖片是在獨立執行緒里進行的,所以它顯示大圖片會更快更流暢,我們可以新建一個View來繼承它,并在這個View里實作我們想要的功能,比如顯示Bitmap,偵聽用戶的觸摸事件主要是通過View的OnTouchListener來實作的,
public void drawBitmap() {
Canvas canvas = surfaceHolder.lockCanvas();
if (canvas != null) {
bitmap = getBitmap();
if (bitmap != null) {
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
Rect rect = new Rect(0, 0, viewWidth, viewHeight);
canvas.drawBitmap(bitmap, null, rect, null);
}
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
int staX = (int) (motionEvent.getX() * getWidthConvert());
int staY = (int) (motionEvent.getY() * getHeightConvert());
MyUtils.setStartX(staX);
MyUtils.setStartY(staY);
touchClientRunnable.setTouchDown(true);
break;
case MotionEvent.ACTION_UP:
int endX = (int) (motionEvent.getX() * getWidthConvert());
int endY = (int) (motionEvent.getY() * getHeightConvert());
MyUtils.setEndX(endX);
MyUtils.setEndY(endY);
touchClientRunnable.setTouchUp(true);
break;
}
return true;
}
@Override
public void run() {
while (isDraw) {
try {
drawBitmap();
setOnTouchListener(this);
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
}
}
(3)TCP傳輸Bitmap
由于截屏的圖片很大,直接傳輸會很慢,所以我們需要對圖片進行壓縮處理,這里采用的是縮放壓縮,
public static Bitmap BitmapMatrixCompress(Bitmap bitmap) {
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
服務端發送Bitmap
private final static byte[] PACKAGE_HEAD = {(byte)0xFF, (byte)0xCF, (byte)0xFA, (byte)0xBF, (byte)0xF6, (byte)0xAF, (byte)0xFE, (byte)0xFF};
public static byte[] BitmaptoBytes(Bitmap bitmap) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);
return baos.toByteArray();
}
private void ServerTransmitBitmap() {
try {
DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
if (bitmap != null) {
byte[] bytes = MyUtils.BitmaptoBytes(bitmap);
dataOutputStream.write(PACKAGE_HEAD);
dataOutputStream.writeInt(MyUtils.getScreenWidth());
dataOutputStream.writeInt(MyUtils.getScreenHeight());
dataOutputStream.writeInt(bytes.length);
dataOutputStream.write(bytes);
}
dataOutputStream.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
客戶端接收Bitmap
private final static byte[] PACKAGE_HEAD = {(byte)0xFF, (byte)0xCF, (byte)0xFA, (byte)0xBF, (byte)0xF6, (byte)0xAF, (byte)0xFE, (byte)0xFF};
public static Bitmap BytestoBitmap(byte[] b) {
if(b.length != 0) {
return BitmapFactory.decodeByteArray(b, 0, b.length);
} else {
return null;
}
}
private void ClientReceiveBitmap() {
try {
InputStream inputStream = socket.getInputStream();
boolean isHead = true;
for (byte b : PACKAGE_HEAD) {
byte head = (byte) inputStream.read();
if (head != b) {
isHead = false;
break;
}
}
if (isHead) {
DataInputStream dataInputStream = new DataInputStream(inputStream);
int width = dataInputStream.readInt();
int height = dataInputStream.readInt();
int len = dataInputStream.readInt();
byte[] bytes = new byte[len];
dataInputStream.readFully(bytes, 0, len);
Bitmap bitmap = MyUtils.BytestoBitmap(bytes);
if (bitmap != null && width != 0 && height != 0) {
if (listener != null) {
listener.onClientReceiveBitmap(bitmap, width, height);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
(4)ADB埠轉發
反向控制主要是用到了adb forward命令進行埠轉發,其實也是TCP通信,使用這種方法主要是手機不用ROOT,
import json
import os
import socket
isConnect = False
isTouch = False
ack = os.popen('adb forward tcp:50003 tcp:50004').read()
if ack.find('error') == 0:
isConnect = False
print('no device')
else:
isConnect = True
if isConnect:
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('127.0.0.1', 50003))
while True:
try:
msg = client.recv(2048)
data = json.loads(msg.decode('utf-8'))
staX = data.get('staX')
staY = data.get('staY')
endX = data.get('endX')
endY = data.get('endY')
action = data.get('action')
if action != 0:
isTouch = True
if isTouch:
cmd = ''
if action == 1:
cmd = 'adb shell input tap {} {}'.format(staX, staY)
elif action == 2:
cmd = 'adb shell input swipe {} {} {} {}'.format(staX, staY, endX, endY)
elif action == 3:
cmd = 'adb shell input keyevent 4'
os.system(cmd)
isTouch = False
action = 0
print(cmd)
except Exception:
continue
以上是部分代碼片段,
三、效果演示

總結
現階段主要是實作了基本功能,還存在很多缺陷,現在只支持在局域網或熱點下共享螢屏,螢屏顯示有很明顯的延遲,反向控制需要連接電腦等,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/398710.html
標籤:其他
