本文主要是讲解车机中控屏内容投屏到后排的pad上,并在pad上点击去操控车机的中控屏的功能
投屏的功能这边就先不讲了,直接开始到反控的功能
调研:
1.adb命令
首先希望通过adb命令去执行输入的事件,adb shell input keyevent 例如 adb shell input 1000,800 但是因为权限的问题,执行的命令不能完成
2.InputManagerService
通过InputManagerService的方式去注入事件,但是验证发现需要给注入事件的app授权,前提是需要拿到车机系统的签名,我们需要拿到车机系统的签名,对注入事件的app做签名这样,它相当于是系统内部的一个app,拥有注入事件的权限(必须要绕过系统检测权限)因为我这边有系统的签名的文件,所以选择这个InputManagerService注入事件
注意需要配置权限
<uses-permission android:name="android.permission.INJECT_EVENTS"
tools:ignore="ProtectedPermissions" />
InputManager方式注入事件的详细过程
首先在反控端,拦截 onTouchEvent(MotionEvent ev)方法,拿到点击的坐标点
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d("lxq", "-----ACTION_DOWN-------触摸点坐标"+"--------------x="+ev.getX()+"-------y===="+ev.getY());
sendInfo(ev.getX(), ev.getY(),TYPE_DOWN);
break;
case MotionEvent.ACTION_MOVE:
Log.d("lxq", "-----ACTION_MOVE-------触摸点坐标"+"--------------x="+ev.getX()+"-------y===="+ev.getY());
sendInfo(ev.getX(), ev.getY(),TYPE_MOVE);
break;
case MotionEvent.ACTION_UP:
Log.d("lxq", "-----ACTION_UP-------触摸点坐标"+"--------------x="+ev.getX()+"-------y===="+ev.getY());
sendInfo(ev.getX(), ev.getY(),TYPE_UP);
break;
}
return super.onTouchEvent(ev);
}
在车机中控屏上和反控的pad端建立socket链接
controlSocket = new Socket(serverAdr, 8008);
private void sendInfo(float x, float y,int type) {
ControlInfoData controlInfoData = new ControlInfoData();
int x1=(int)x;
int y1=(int)y;
controlInfoData.setX(x1);
controlInfoData.setY(y1);
controlInfoData.setType(type);
controlInfoData.setWidth(widthPixels);
controlInfoData.setHeight(heightPixels);
new Thread(
new Runnable() {
@Override
public void run() {
sendControlInfo(controlInfoData);
}
}
).start();
}
以上发送的信息有点击的坐标点还包括反控pad端的屏幕宽高信息,还有当前的事件类型,down ,up ,move,
public void sendControlInfo(ControlInfoData data){
OutputStream outputStream = null;
try {
if (scrcpy.getControlSocket()!=null&&scrcpy.getControlSocket().getOutputStream()!=null&&isConnection()){
outputStream = scrcpy.getControlSocket().getOutputStream();
Gson gson=new Gson();
String info = gson.toJson(data);
if (outputStream!=null){
outputStream.write(info.getBytes());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
以上是打包数据发送到车机中控屏端
车机端(被反控端)
和pad端建立sokcet 链接等待pad 端发过来点击的数据和屏幕信息()
private static void listenAndAccept() throws IOException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Runnable() {
@Override
public void run() {
if(serverSocket==null){
Log.e("lxq", "----serverSocket==null--------------------");
try {
serverSocket = new ServerSocket(8008);
while (true) {
try {
Socket socket1 = serverSocket.accept();
Log.e("lxq", "------serverSocket.accept()------------");
new Thread(new Runnable() {
@Override
public void run() {
InputStream inputStream = null;
try {
while (true){
inputStream = socket1.getInputStream();
prepareData(inputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
} catch (IOException e) {
Log.e("lxq", "------serverSocket.accept()----Exception="+e.getMessage());
throw new RuntimeException(e);
}
}else {
Log.e("lxq", "------serverSocket!!!!==null--------------------");
}
}
});
}
不断的监听拿到发过来数据去做先转化操作:
private static void prepareData(InputStream inputStream) throws IOException {
StringBuilder stringBuilder=new StringBuilder();
byte[] buffer = new byte[2048];
int bytesRead = inputStream.read(buffer);
if (bytesRead<0){
return;
}
String message = new String(buffer, 0, bytesRead);
//以下是解决socket高频发消息的沾包问题,将收到的多次发送并一次收到的数据拼接成标准的JSON数组然后在解析成jsonArray
String[] split1 = message.split("\\{");
if (split1.length == 1) {
stringBuilder.append("[").append("{").append(split1[1]).append("]");
Log.e("lxq", "---------------data.length == -------"+stringBuilder.toString());
} else {
String[] data=new String[split1.length-1];
for (int i=0 ;i<split1.length-1;i++){
data[i]=split1[i+1];
}
for (int i=0 ;i<data.length;i++){
}
for (int i = 0; i <data.length; i++) {
if (i == 0) {
stringBuilder.append("[");
}
stringBuilder.append("{").append(data[i]);
if (i!= data.length - 1)
stringBuilder.append(",");
if (i == data.length - 1) {
stringBuilder.append("]");
}
}
}
//将处理好的JSON数组然后在解析成jsonArray
JsonArray jsonArray = new JsonParser().parse(stringBuilder.toString()).getAsJsonArray();
List<ControlInfoData> ControlInfoDataList = new ArrayList<>();
for (JsonElement element : jsonArray) {
ControlInfoData infoData = new Gson().fromJson(element, ControlInfoData.class);
ControlInfoDataList.add(infoData);
}
Log.e("lxq", "---------------lxq----ControlInfoDataList.size()-------"+ControlInfoDataList.size());
if (ControlInfoDataList.size() > 0) {//将接收到的单个或者多个点击数据按顺序执行点击事件
HandleAction(ControlInfoDataList);
ControlInfoDataList.clear();
}}
以上是解析数据,但是存在一个问题在反控端不断发数据的过程中会出现一个沾包现象就是本来发的是{x:"1000",y:""800,width:"1080",height:,"2000"} 在快速发送数据会变成{x:"1000",y:""800,width:"1080",height:,"2000"} {x:"1000",y:""800,width:"1080",height:,"2000"}连在一起,所以我手动在了中间的","转化成jsonarray数组去解析数据, 一个条目是一个动作
public static void HandleAction(List<ControlInfoData> ControlInfoDataList){
for (int i=0;i<ControlInfoDataList.size();i++){
injectAction(ControlInfoDataList.get(i));
}
}
public static void injectAction(ControlInfoData infoData){
LogicReckon logicReckon=new LogicReckon();
logicReckon.setmLocalDeviceWidth(mDeviceWidth);
logicReckon.setmLocalDeviceHeight(mDeviceHeight);
logicReckon.setmRemoteDeviceWidth(infoData.getWidth());
logicReckon.setmRemoteDeviceHeight(infoData.getHeight());
LogicReckon.PointDevice point= logicReckon.convert((float)infoData.getX(),(float)infoData.getY());
if (point != null) {
injectClickActionDown(point.getX(), point.getY());
injectClickActionUp(point.getX(), point.getY());
}
switch (infoData.getType()) {
case TYPE_DOWN:
Log.e("lxq", "down-----原来的坐标====x=" + infoData.getX() + "y==" + infoData.getY() + "----转化后的--x===" + point.getX() + "--y====" + point.getY());
injectClickActionDown(point.getX(), point.getY());
break;
case TYPE_UP:
Log.e("lxq", "up-----原来的坐标====x=" + infoData.getX() + "y==" + infoData.getY() + "----转化后的--x===" + point.getX() + "--y====" + point.getY());
if(point!=null){
if (!moveEnd){
injectClickActionDown(point.getX(), point.getY());
injectClickActionUp(point.getX(), point.getY());
}else {
injectClickActionMove(point.getX(), point.getY());
injectClickActionUp(point.getX(), point.getY());
moveEnd=false;
}
}
break;
case TYPE_MOVE:
if (point != null) {
Log.e("lxq", "move----原来的坐标====x=" + infoData.getX() + "y==" + infoData.getY() + "----转化后的--x===" + point.getX() + "--y====" + point.getY());
if (moveEnd = false) {
moveEnd = true;
injectClickActionDown(point.getX(), point.getY());
}
injectClickActionMove(point.getX(), point.getY());
}
break;
}
}
down事件
// 模拟点击屏幕 (x,y)
public static void injectClickActionDown(float x, float y) {
MotionEvent evenDownt = MotionEvent.obtain(System.currentTimeMillis(),
System.currentTimeMillis() +5,
MotionEvent.ACTION_DOWN,
x, y, 0);
evenDownt.setSource(InputDevice.SOURCE_TOUCHSCREEN);
invokeInjectInputEvent(evenDownt,1);
Log.e("TAG", "lxq-----------------------------injectClickActionDown");
}
// 模拟点击屏幕 (x,y)
public static void injectClickActionUp(float x, float y) {
// 抬起
MotionEvent eventUp = MotionEvent.obtain(System.currentTimeMillis(),
System.currentTimeMillis() +5,
MotionEvent.ACTION_UP,
x, y, 0);
eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
invokeInjectInputEvent(eventUp,1);
Log.e("TAG", "lxq-----------------------------injectClickActionUp");
}
move事件:
// 模拟点击屏幕 (x,y)
public static void injectClickActionMove(float x, float y) {
// 抬起
MotionEvent eventUp = MotionEvent.obtain(System.currentTimeMillis(),
System.currentTimeMillis() +5,
MotionEvent.ACTION_MOVE,
x, y, 0);
eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
invokeInjectInputEvent(eventUp,1);
Log.e("TAG", "lxq-----------------------------injectClickActionMove");
}
注意如果是move事件的话,中间要连续执行多个move事件,然后在执行up Event
用一个后台的service 去监听收到的反控标,然后执行任务
/**
* 传递反控事件用的
* */
public class InputService extends Service {
static ServerSocket serverSocket = null;
private static void invokeInjectInputEvent(MotionEvent event, int type) {
Class cl = InputManager.class;
try {
Method method = cl.getMethod("getInstance");
Object result = method.invoke(cl);
InputManager im = (InputManager) result;
method = cl.getMethod("injectInputEvent", InputEvent.class, int.class);
method.invoke(im, event, type);
Log.e("TAG", "lxq-----------------------------invokeInjectInputEvent");
Method recycleMehod = event.getClass().getMethod("recycle");
recycleMehod.invoke(event);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 模拟点击屏幕 (x,y)
public static void injectClickActionDown(float x, float y) {
MotionEvent evenDownt = MotionEvent.obtain(System.currentTimeMillis(),
System.currentTimeMillis() +5,
MotionEvent.ACTION_DOWN,
x, y, 0);
evenDownt.setSource(InputDevice.SOURCE_TOUCHSCREEN);
invokeInjectInputEvent(evenDownt,1);
Log.e("TAG", "lxq-----------------------------injectClickActionDown");
}
// 模拟点击屏幕 (x,y)
public static void injectClickActionUp(float x, float y) {
// 抬起
MotionEvent eventUp = MotionEvent.obtain(System.currentTimeMillis(),
System.currentTimeMillis() +5,
MotionEvent.ACTION_UP,
x, y, 0);
eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
invokeInjectInputEvent(eventUp,1);
Log.e("TAG", "lxq-----------------------------injectClickActionUp");
}
// 模拟点击屏幕 (x,y)
public static void injectClickActionMove(float x, float y) {
// 抬起
MotionEvent eventUp = MotionEvent.obtain(System.currentTimeMillis(),
System.currentTimeMillis() +5,
MotionEvent.ACTION_MOVE,
x, y, 0);
eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
invokeInjectInputEvent(eventUp,1);
Log.e("TAG", "lxq-----------------------------injectClickActionMove");
}
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
Log.e("TAG", "lxq-----------------------------onStartCommand");
try {
listenAndAccept();
Log.e("lxq", "------serverSocket------listenAndAccept------------------");
} catch (IOException e) {
Log.e("lxq", "------serverSocket------listenAndAccept-----Exception===-------------"+e.getMessage());
throw new RuntimeException(e);
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onCreate() {
Log.e("TAG", "lxq-------Service----------------------onCreate");
super.onCreate();
}
@Override
public void onDestroy() {
Log.e("TAG", "lxq---------Service--------------------onDestroy!!!!");
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.e("TAG", "lxq---------Service--------------------onBind");
return null;
}
@Override
public void onStart(@Nullable Intent intent, int startId) {
Log.e("TAG", "lxq---------Service--------------------onStart");
super.onStart(intent, startId);
}
private static void listenAndAccept() throws IOException {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Runnable() {
@Override
public void run() {
if(serverSocket==null){
Log.e("lxq", "----serverSocket==null--------------------");
try {
serverSocket = new ServerSocket(8008);
while (true) {
try {
Socket socket1 = serverSocket.accept();
Log.e("lxq", "------serverSocket.accept()------------");
new Thread(new Runnable() {
@Override
public void run() {
InputStream inputStream = null;
try {
while (true){
inputStream = socket1.getInputStream();
prepareData(inputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}).start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
} catch (IOException e) {
Log.e("lxq", "------serverSocket.accept()----Exception="+e.getMessage());
throw new RuntimeException(e);
}
}else {
Log.e("lxq", "------serverSocket!!!!==null--------------------");
}
}
});
}
private static void prepareData(InputStream inputStream) throws IOException {
StringBuilder stringBuilder=new StringBuilder();
byte[] buffer = new byte[2048];
int bytesRead = inputStream.read(buffer);
if (bytesRead<0){
return;
}
String message = new String(buffer, 0, bytesRead);
//以下是解决socket高频发消息的沾包问题,将收到的多次发送并一次收到的数据拼接成标准的JSON数组然后在解析成jsonArray
String[] split1 = message.split("\\{");
if (split1.length == 1) {
stringBuilder.append("[").append("{").append(split1[1]).append("]");
Log.e("lxq", "---------------data.length == -------"+stringBuilder.toString());
} else {
String[] data=new String[split1.length-1];
for (int i=0 ;i<split1.length-1;i++){
data[i]=split1[i+1];
}
for (int i=0 ;i<data.length;i++){
}
for (int i = 0; i <data.length; i++) {
if (i == 0) {
stringBuilder.append("[");
}
stringBuilder.append("{").append(data[i]);
if (i!= data.length - 1)
stringBuilder.append(",");
if (i == data.length - 1) {
stringBuilder.append("]");
}
}
}
//将处理好的JSON数组然后在解析成jsonArray
JsonArray jsonArray = new JsonParser().parse(stringBuilder.toString()).getAsJsonArray();
List<ControlInfoData> ControlInfoDataList = new ArrayList<>();
for (JsonElement element : jsonArray) {
ControlInfoData infoData = new Gson().fromJson(element, ControlInfoData.class);
ControlInfoDataList.add(infoData);
}
Log.e("lxq", "---------------lxq----ControlInfoDataList.size()-------"+ControlInfoDataList.size());
if (ControlInfoDataList.size() > 0) {//将接收到的单个或者多个点击数据按顺序执行点击事件
HandleAction(ControlInfoDataList);
ControlInfoDataList.clear();
}}
public static void HandleAction(List<ControlInfoData> ControlInfoDataList){
for (int i=0;i<ControlInfoDataList.size();i++){
injectAction(ControlInfoDataList.get(i));
}
}
public static void injectAction(ControlInfoData infoData){
LogicReckon logicReckon=new LogicReckon();
logicReckon.setmLocalDeviceWidth(mDeviceWidth);
logicReckon.setmLocalDeviceHeight(mDeviceHeight);
logicReckon.setmRemoteDeviceWidth(infoData.getWidth());
logicReckon.setmRemoteDeviceHeight(infoData.getHeight());
LogicReckon.PointDevice point= logicReckon.convert((float)infoData.getX(),(float)infoData.getY());
if (point != null) {
injectClickActionDown(point.getX(), point.getY());
injectClickActionUp(point.getX(), point.getY());
}
switch (infoData.getType()) {
case TYPE_DOWN:
Log.e("lxq", "down-----原来的坐标====x=" + infoData.getX() + "y==" + infoData.getY() + "----转化后的--x===" + point.getX() + "--y====" + point.getY());
injectClickActionDown(point.getX(), point.getY());
break;
case TYPE_UP:
Log.e("lxq", "up-----原来的坐标====x=" + infoData.getX() + "y==" + infoData.getY() + "----转化后的--x===" + point.getX() + "--y====" + point.getY());
if(point!=null){
if (!moveEnd){
injectClickActionDown(point.getX(), point.getY());
injectClickActionUp(point.getX(), point.getY());
}else {
injectClickActionMove(point.getX(), point.getY());
injectClickActionUp(point.getX(), point.getY());
moveEnd=false;
}
}
break;
case TYPE_MOVE:
if (point != null) {
Log.e("lxq", "move----原来的坐标====x=" + infoData.getX() + "y==" + infoData.getY() + "----转化后的--x===" + point.getX() + "--y====" + point.getY());
if (moveEnd = false) {
moveEnd = true;
injectClickActionDown(point.getX(), point.getY());
}
injectClickActionMove(point.getX(), point.getY());
}
break;
}
}
}
以下类是用来做坐标转换的 ,从pad端到车机中控屏端,不同的屏幕大小需要做坐标转换:
package com.lenovo.startjar;
import android.content.Context;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
/**
* Created by lee 2023/11/29
*/
public class LogicReckon {
private int mRemoteDeviceWidth ;
private int mRemoteDeviceHeight;
private int mLocalDeviceWidth;
private int mLocalDeviceHeight;
private int mOrientation=1;
private Context mContext=null;
public LogicReckon() {
}
public int getmRemoteDeviceWidth() {
return mRemoteDeviceWidth;
}
public void setmRemoteDeviceWidth(int mRemoteDeviceWidth) {
this.mRemoteDeviceWidth = mRemoteDeviceWidth;
}
public int getmRemoteDeviceHeight() {
return mRemoteDeviceHeight;
}
public void setmRemoteDeviceHeight(int mRemoteDeviceHeight) {
this.mRemoteDeviceHeight = mRemoteDeviceHeight;
}
public int getmLocalDeviceWidth() {
return mLocalDeviceWidth;
}
public void setmLocalDeviceWidth(int mLocalDeviceWidth) {
this.mLocalDeviceWidth = mLocalDeviceWidth;
}
public int getmLocalDeviceHeight() {
return mLocalDeviceHeight;
}
public void setmLocalDeviceHeight(int mLocalDeviceHeight) {
this.mLocalDeviceHeight = mLocalDeviceHeight;
}
public void setOrientation(int val)
{
// WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
mLocalDeviceWidth = 1824;
mLocalDeviceHeight = 1080;
mOrientation=val;
}
public PointDevice convert(float x,float y)
{
PointDevice point=new PointDevice();
double scaling=(mLocalDeviceHeight*(1.0d)/mLocalDeviceWidth);
double scalingHeight=mLocalDeviceHeight*(1.0d)/mRemoteDeviceHeight;
double scalingWidth=mLocalDeviceWidth*(1.0d)/mRemoteDeviceWidth;
double height= (scaling*mRemoteDeviceWidth);
double width= (mRemoteDeviceHeight/scaling);
double realheight=0;
double realwidth=0;
double Ay=0;
double Ax=0;
//取远程设备的宽做基准
if(height<=mRemoteDeviceHeight)
{
realwidth=mRemoteDeviceWidth;
realheight=height;
Ay=(mRemoteDeviceHeight-height)/2;
if((y-Ay)<0||(y-Ay)>realheight)
return null;
scalingHeight=mLocalDeviceHeight*(1.0d)/realheight;
point.setX((float) (x * scalingWidth)).setY((float) ((y-Ay)*scalingHeight));
}
//取远程设备的高为做基准
else if(width<=mRemoteDeviceWidth)
{
realwidth=width;
realheight=mRemoteDeviceHeight;
Ax=(mRemoteDeviceWidth-width)/2;
if((x-Ax)<0||(x-Ax)>realwidth)
return null;
scalingWidth=mLocalDeviceWidth*(1.0d)/realwidth;
point.setX((float) ((x-Ax) * scalingWidth)).setY((float) (y * scalingHeight));
}
return point;
}
public class PointDevice{
private float x;
private float y;
public float getX() {
return x;
}
public PointDevice setX(float x) {
this.x = x;
return this;
}
public float getY() {
return y;
}
public PointDevice setY(float y) {
this.y = y;
return this;
}
}
}