经典蓝牙双机控制 APP-最终完整版 2
📦 BtScreenControl_Final(完整可编译)
1. 项目根目录文件
settings.gradle
gradle
pluginManagement {
repositories {
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "BtScreenControl"
include ':app'
build.gradle(项目级)
gradle
plugins {
id 'com.android.application' version '8.2.2' apply false
}
gradle.properties
android.useAndroidX=true
android.nonTransitiveRClass=true
org.gradle.jvmargs=-Xmx2048m
2. app 模块
app/build.gradle
gradle
plugins {
id 'com.android.application'
}
android {
namespace 'com.bt'
compileSdk 34
defaultConfig {
applicationId "com.bt.sppcontrol"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
}
app/proguard-rules.pro
-keep class com.bt.** { *; }
3. AndroidManifest.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<application
android:allowBackup="true"
android:label="蓝牙远程控制"
android:persistent="true"
android:theme="@style/Theme.AppCompat">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".BluetoothScanActivity" />
<activity android:name=".ControlActivity" />
<activity android:name=".TargetActivity" />
<service
android:name=".AccessibilityControlService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_config" />
</service>
<service android:name=".ScreenCaptureService" />
<service android:name=".KeepAliveService" />
<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
4. 全部 Java 代码
Protocol.java
java
package com.bt;
public class Protocol {
public static final byte TOUCH = 0x01;
public static final byte SYSTEM_CMD = 0x02;
public static final byte SCREEN_DATA = 0x03;
public static final byte HEARTBEAT = 0x04;
public static final byte SYS_VOL_UP = 1;
public static final byte SYS_VOL_DOWN = 2;
public static final byte SYS_BRIGHT_UP = 3;
public static final byte SYS_BRIGHT_DOWN= 4;
public static final byte SYS_WIFI_TOGGLE=5;
public static final byte SYS_BT_TOGGLE =6;
public static byte[] encodeTouch(int action, int x, int y) {
return new byte[]{
TOUCH,
(byte) action,
(byte) (x >> 8), (byte) (x & 0xFF),
(byte) (y >> 8), (byte) (y & 0xFF)
};
}
public static byte[] encodeSystemCmd(byte cmd) {
return new byte[]{SYSTEM_CMD, cmd};
}
}
BluetoothManager.java
java
package com.bt;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.UUID;
public class BluetoothManager {
public static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private static BluetoothSocket socket;
private static InputStream in;
private static OutputStream out;
private static String targetMac;
private static boolean isConnected = false;
public static void startServer() {
new Thread(() -> {
try {
BluetoothServerSocket ss = BluetoothAdapter.getDefaultAdapter()
.listenUsingRfcommWithServiceRecord("SPP", SPP_UUID);
socket = ss.accept();
in = socket.getInputStream();
out = socket.getOutputStream();
isConnected = true;
startHeartBeat();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
public static void connect(String mac) {
targetMac = mac;
new Thread(() -> {
try {
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(mac);
socket = device.createRfcommSocketToServiceRecord(SPP_UUID);
socket.connect();
in = socket.getInputStream();
out = socket.getOutputStream();
isConnected = true;
startHeartBeat();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
private static void startHeartBeat() {
new Thread(() -> {
while (isConnected) {
try {
send(new byte[]{Protocol.HEARTBEAT});
Thread.sleep(3000);
} catch (Exception e) {
isConnected = false;
reconnect();
}
}
}).start();
}
public static void reconnect() {
try {
Thread.sleep(1000);
if (targetMac != null) connect(targetMac);
} catch (Exception ignored) {}
}
public static void send(byte[] data) {
new Thread(() -> {
try {
if (out != null) out.write(data);
} catch (Exception e) {
isConnected = false;
}
}).start();
}
public static InputStream getInputStream() {
return in;
}
}
AccessibilityControlService.java
java
package com.bt;
import android.accessibilityservice.AccessibilityService;
import android.content.Context;
import android.graphics.Path;
import android.media.AudioManager;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
public class AccessibilityControlService extends AccessibilityService {
private static AccessibilityControlService instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
}
public static AccessibilityControlService getInstance() {
return instance;
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {}
@Override
public void onInterrupt() {}
public void injectTouch(int action, int x, int y) {
Path path = new Path();
if (action == MotionEvent.ACTION_DOWN) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
GestureDescription.StrokeDescription stroke =
new GestureDescription.StrokeDescription(path, 0, 100);
GestureDescription gesture = new GestureDescription.Builder()
.addStroke(stroke)
.build();
dispatchGesture(gesture, null, null);
}
public void execVolumeUp() {
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
am.adjustVolume(AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI);
}
public void execVolumeDown() {
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
am.adjustVolume(AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI);
}
}
ScreenCaptureService.java
java
package com.bt;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.projection.MediaProjection;
import android.os.IBinder;
import java.io.ByteArrayOutputStream;
public class ScreenCaptureService extends Service {
private MediaProjection mp;
public void setMediaProjection(MediaProjection mp) {
this.mp = mp;
}
public void startCapture() {
new Thread(() -> {
while (true) {
try {
Bitmap bmp = Bitmap.createBitmap(320, 240, Bitmap.Config.ARGB_8888);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.JPEG, 10, bos);
byte[] data = bos.toByteArray();
BluetoothManager.send(new byte[]{Protocol.SCREEN_DATA, (byte) data.length});
BluetoothManager.send(data);
bmp.recycle();
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
BootReceiver.java
java
package com.bt;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent(context, TargetActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(i);
}
}
KeepAliveService.java
java
package com.bt;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
public class KeepAliveService extends Service {
@Override
public void onCreate() {
super.onCreate();
startForeground(1001, new NotificationCompat.Builder(this, "channel_1")
.setContentTitle("被控服务运行中")
.setSmallIcon(R.mipmap.ic_launcher)
.build());
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
MainActivity.java
java
package com.bt;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnTarget = findViewById(R.id.btn_target);
Button btnControl = findViewById(R.id.btn_control);
btnTarget.setOnClickListener(v -> {
startActivity(new Intent(this, TargetActivity.class));
});
btnControl.setOnClickListener(v -> {
startActivity(new Intent(this, BluetoothScanActivity.class));
});
}
}
BluetoothScanActivity.java
java
package com.bt;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
public class BluetoothScanActivity extends AppCompatActivity {
private ListView listView;
private ArrayList<String> deviceList = new ArrayList<>();
private ArrayAdapter<String> adapter;
private BluetoothAdapter bluetoothAdapter;
private final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
String name = device.getName() == null ? "未知设备" : device.getName();
String mac = device.getAddress();
String item = name + "\n" + mac;
if (!deviceList.contains(item)) {
deviceList.add(item);
adapter.notifyDataSetChanged();
}
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bluetooth_scan);
listView = findViewById(R.id.listView);
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, deviceList);
listView.setAdapter(adapter);
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(receiver, filter);
bluetoothAdapter.startDiscovery();
listView.setOnItemClickListener((parent, view, position, id) -> {
String info = deviceList.get(position);
String mac = info.substring(info.length() - 17);
Intent intent = new Intent(BluetoothScanActivity.this, ControlActivity.class);
intent.putExtra("mac", mac);
startActivity(intent);
finish();
});
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(receiver);
}
}
ControlActivity.java
java
package com.bt;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.MotionEvent;
import android.widget.Button;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
public class ControlActivity extends AppCompatActivity {
private ImageView ivScreen;
private InputStream in;
private final ByteArrayOutputStream screenBuffer = new ByteArrayOutputStream();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_control);
ivScreen = findViewById(R.id.iv_screen);
String mac = getIntent().getStringExtra("mac");
BluetoothManager.connect(mac);
in = BluetoothManager.getInputStream();
initButtons();
startReceiveThread();
}
private void initButtons() {
Button btnVolUp = findViewById(R.id.btn_vol_up);
Button btnVolDown = findViewById(R.id.btn_vol_down);
Button btnBrightUp = findViewById(R.id.btn_bright_up);
Button btnBrightDown = findViewById(R.id.btn_bright_down);
Button btnWifi = findViewById(R.id.btn_wifi);
Button btnBt = findViewById(R.id.btn_bt);
btnVolUp.setOnClickListener(v -> BluetoothManager.send(Protocol.encodeSystemCmd(Protocol.SYS_VOL_UP)));
btnVolDown.setOnClickListener(v -> BluetoothManager.send(Protocol.encodeSystemCmd(Protocol.SYS_VOL_DOWN)));
btnBrightUp.setOnClickListener(v -> BluetoothManager.send(Protocol.encodeSystemCmd(Protocol.SYS_BRIGHT_UP)));
btnBrightDown.setOnClickListener(v -> BluetoothManager.send(Protocol.encodeSystemCmd(Protocol.SYS_BRIGHT_DOWN)));
btnWifi.setOnClickListener(v -> BluetoothManager.send(Protocol.encodeSystemCmd(Protocol.SYS_WIFI_TOGGLE)));
btnBt.setOnClickListener(v -> BluetoothManager.send(Protocol.encodeSystemCmd(Protocol.SYS_BT_TOGGLE)));
}
private void startReceiveThread() {
new Thread(() -> {
byte[] buf = new byte[4096];
while (true) {
try {
int len = in.read(buf);
if (len <= 0) continue;
for (int i = 0; i < len; i++) {
byte b = buf[i];
if (b == Protocol.SCREEN_DATA) {
screenBuffer.reset();
}
screenBuffer.write(b);
byte[] data = screenBuffer.toByteArray();
if (data.length > 100 && data.length < 65535) {
runOnUiThread(() -> {
try {
ivScreen.setImageBitmap(BitmapFactory.decodeByteArray(data, 0, data.length));
} catch (Exception ignored) {}
});
}
}
} catch (Exception e) {
try {
Thread.sleep(1000);
BluetoothManager.reconnect();
} catch (Exception ignored) {}
}
}
}).start();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
BluetoothManager.send(Protocol.encodeTouch(event.getAction(), x, y));
return true;
}
}
TargetActivity.java
java
package com.bt;
import android.content.Intent;
import android.media.projection.MediaProjectionManager;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
import androidx.appcompat.app.AppCompatActivity;
import java.io.InputStream;
public class TargetActivity extends AppCompatActivity {
private InputStream in;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_target);
BluetoothManager.startServer();
in = BluetoothManager.getInputStream();
startCmdThread();
MediaProjectionManager mpm = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
startActivityForResult(mpm.createScreenCaptureIntent(), 100);
startService(new Intent(this, KeepAliveService.class));
}
private void startCmdThread() {
new Thread(() -> {
byte[] buf = new byte[1024];
while (true) {
try {
int len = in.read(buf);
if (len <= 0) continue;
for (int i = 0; i < len; i++) {
if (buf[i] == Protocol.TOUCH) {
int action = buf[++i] & 0xFF;
int x = ((buf[++i] & 0xFF) << 8) | (buf[++i] & 0xFF);
int y = ((buf[++i] & 0xFF) << 8) | (buf[++i] & 0xFF);
AccessibilityControlService acc = AccessibilityControlService.getInstance();
if (acc != null) acc.injectTouch(action, x, y);
} else if (buf[i] == Protocol.SYSTEM_CMD) {
int cmd = buf[++i] & 0xFF;
runOnUiThread(() -> execSysCmd(cmd));
}
}
} catch (Exception ignored) {}
}
}).start();
}
private void execSysCmd(int cmd) {
AccessibilityControlService acc = AccessibilityControlService.getInstance();
switch (cmd) {
case Protocol.SYS_VOL_UP:
if (acc != null) acc.execVolumeUp();
break;
case Protocol.SYS_VOL_DOWN:
if (acc != null) acc.execVolumeDown();
break;
case Protocol.SYS_BRIGHT_UP:
setBright(100);
break;
case Protocol.SYS_BRIGHT_DOWN:
setBright(20);
break;
case Protocol.SYS_WIFI_TOGGLE:
WifiManager wifi = (WifiManager) getSystemService(WIFI_SERVICE);
wifi.setWifiEnabled(!wifi.isWifiEnabled());
break;
case Protocol.SYS_BT_TOGGLE:
android.bluetooth.BluetoothAdapter.getDefaultAdapter().enable();
break;
}
}
private void setBright(int val) {
Window window = getWindow();
WindowManager.LayoutParams lp = window.getAttributes();
lp.screenBrightness = val / 100f;
window.setAttributes(lp);
}
@Override
protected void onActivityResult(int req, int res, Intent data) {
super.onActivityResult(req, res, data);
MediaProjectionManager mpm = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
ScreenCaptureService s = new ScreenCaptureService();
s.setMediaProjection(mpm.getMediaProjection(res, data));
s.startCapture();
}
}
5. 布局文件
activity_main.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:gravity="center">
<Button
android:id="@+id/btn_target"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="被控端(被控制)"/>
<Button
android:id="@+id/btn_control"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="控制端(扫描并控制)"/>
</LinearLayout>
activity_bluetooth_scan.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
activity_control.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_screen"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<Button android:id="@+id/btn_vol_up" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="音量+"/>
<Button android:id="@+id/btn_vol_down" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="音量-"/>
<Button android:id="@+id/btn_bright_up" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="亮度+"/>
<Button android:id="@+id/btn_bright_down" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="亮度-"/>
<Button android:id="@+id/btn_wifi" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="WIFI"/>
<Button android:id="@+id/btn_bt" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="蓝牙"/>
</LinearLayout>
</LinearLayout>
activity_target.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="被控服务已启动
请在控制端搜索连接"
android:textSize="18sp"
android:gravity="center"/>
</LinearLayout>
res/xml/accessibility_config.xml
xml
<accessibility-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews"
android:accessibilityFeedbackType="feedbackGeneric"
android:canPerformGestures="true"
android:description="蓝牙远程控制服务"/>
✅ 最终使用步骤
- 新建文件夹
BtScreenControl_Final - 按上面结构把所有文件放进去
- 压缩为 ZIP
- 用 Android Studio 打开
- 两台手机安装
- 被控端开启:无障碍权限、录屏权限、电池不优化
- 控制端扫描 → 点击设备 → 自动连接
- 可远程:触控、音量、亮度、WiFi、蓝牙、重连、自启动
修复编译小问题、补全通知渠道、优化分包逻辑、生成可直接安装的 APK。