parent
eb98dc0826
commit
aaf76a2ab7
@ -0,0 +1 @@
|
||||
/build
|
@ -0,0 +1,42 @@
|
||||
plugins {
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-parcelize'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.android.compileSdkVersion
|
||||
buildToolsVersion rootProject.ext.android.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion rootProject.ext.android.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.android.targetSdkVersion
|
||||
versionCode rootProject.ext.android.versionCode
|
||||
versionName rootProject.ext.android.versionName
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
viewBinding {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':commonLib')
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
|
||||
// 添加kotlin依赖
|
||||
implementation rootProject.ext.dependencies.kotlin
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.common.bluetooth">
|
||||
|
||||
<!-- 检测蓝牙状态 -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth_le"
|
||||
android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<!-- 增加蓝牙所需要的权限 -->
|
||||
<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.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true">
|
||||
<activity
|
||||
android:name="com.common.bluetooth.BtDemoActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat"/>
|
||||
<service
|
||||
android:name="com.common.bluetooth.service.BLEReceiveService"
|
||||
android:exported="true"
|
||||
android:enabled="true">
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="com.common.bluetooth.service.BLEClientService"
|
||||
android:exported="true"
|
||||
android:enabled="true">
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
@ -0,0 +1,99 @@
|
||||
package com.common.bluetooth
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* BT静态方法
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-14
|
||||
*/
|
||||
object BtConstants {
|
||||
const val BT_NAME = "innovation bt"
|
||||
|
||||
/**
|
||||
* 蓝牙类型
|
||||
*/
|
||||
enum class BLUETOOTH_TYPE {
|
||||
/**
|
||||
* 经典蓝牙
|
||||
*/
|
||||
CLASSIC,
|
||||
|
||||
/**
|
||||
* 低功耗蓝牙
|
||||
*/
|
||||
BLE
|
||||
}
|
||||
|
||||
/**
|
||||
* 蓝牙类型
|
||||
*/
|
||||
enum class CLIENT_TYPE {
|
||||
/**
|
||||
* 服务端
|
||||
*/
|
||||
SERVER,
|
||||
|
||||
/**
|
||||
* 客户端
|
||||
*/
|
||||
CLIENT
|
||||
}
|
||||
|
||||
private const val STATUS_BASE = 0x1
|
||||
const val CONNECT_SUCCESS = STATUS_BASE shl 1
|
||||
const val CONNECT_ERROR = STATUS_BASE shl 2
|
||||
const val READ_SUCCESS = STATUS_BASE shl 3
|
||||
const val READ_ERROR = STATUS_BASE shl 4
|
||||
const val WRITE_SUCCESS = STATUS_BASE shl 5
|
||||
const val WRITE_ERROR = STATUS_BASE shl 6
|
||||
const val CREATE_SERVER_SUCCESS = STATUS_BASE shl 7
|
||||
const val CREATE_SERVER_ERROR = STATUS_BASE shl 8
|
||||
const val DISCONNECT = STATUS_BASE shl 9
|
||||
|
||||
enum class EXCEPTION(s: String) {
|
||||
NOT_CONNECTED("not connect"),
|
||||
NULL_SERVICE("service is null"),
|
||||
NULL_CHARACTERISTIC("characteristic is null"),
|
||||
READ_EXCEPTION("characteristic read error"),
|
||||
WRITE_EXCEPTION("characteristic write error"),
|
||||
NOTIFY_OPEN_EXCEPTION("notification open error"),
|
||||
NOTIFY_CLOSE_EXCEPTION("notification close error")
|
||||
}
|
||||
|
||||
// Message types sent from the BluetoothChatService Handler
|
||||
const val MESSAGE_STATE_CHANGE = 1
|
||||
const val MESSAGE_READ = 2
|
||||
const val MESSAGE_WRITE = 3
|
||||
const val MESSAGE_DEVICE_NAME = 4
|
||||
const val MESSAGE_TOAST = 5
|
||||
|
||||
// Key names received from the BluetoothChatService Handler
|
||||
const val DEVICE_NAME = "device_name"
|
||||
const val TOAST = "toast"
|
||||
|
||||
/**
|
||||
* 最大的单包发送字节数
|
||||
*/
|
||||
const val MAX_MTU = 35
|
||||
|
||||
val UUID_SERVICE = UUID.fromString("66f564dc-121f-3e7f-80b1-f005d3f194c9")
|
||||
val UUID_CHARACTERISTIC_NOTIFY = UUID.fromString("66f564dd-121f-3e7f-80b1-f005d3f194c9")
|
||||
val UUID_CHARACTERISTIC_WRITE = UUID.fromString("66f564de-121f-3e7f-80b1-f005d3f194c9")
|
||||
val UUID_DESCRIPTOR = UUID.fromString("66f564df-121f-3e7f-80b1-f005d3f194c9")
|
||||
|
||||
/**
|
||||
* 获取UUID
|
||||
*/
|
||||
fun getUUid(context: Context?): UUID {
|
||||
if (context == null) {
|
||||
return UUID.randomUUID()
|
||||
}
|
||||
val androidId =
|
||||
Settings.System.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
|
||||
return UUID.nameUUIDFromBytes(androidId.toByteArray())
|
||||
}
|
||||
}
|
@ -0,0 +1,191 @@
|
||||
package com.common.bluetooth;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
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.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.common.bluetooth.bean.AssociateEvent;
|
||||
import com.common.bluetooth.bean.AssociateSourceEvent;
|
||||
import com.common.bluetooth.bean.CommonMsg;
|
||||
import com.common.bluetooth.bean.KeyboardEvent;
|
||||
import com.common.bluetooth.callback.BLEClientListener;
|
||||
import com.common.bluetooth.databinding.ActivityMainBinding;
|
||||
import com.common.bluetooth.view.BtDeviceListAdapter;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 蓝牙样例代码
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-11-2
|
||||
*/
|
||||
public class BtDemoActivity extends AppCompatActivity {
|
||||
private static final String TAG = "BtMainActivity";
|
||||
/**
|
||||
* 启动蓝牙请求码
|
||||
*/
|
||||
private static final int REQUEST_ENABLE_BT = 0;
|
||||
|
||||
/**
|
||||
* 获取位置权限
|
||||
*/
|
||||
private static final int PERMISSION_REQUEST_LOCATION = 1;
|
||||
|
||||
private ActivityMainBinding mBinding = null;
|
||||
private BtDeviceListAdapter btDeviceListAdapter = null;
|
||||
private final ArrayList<BluetoothDevice> bluetoothDevices = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mBinding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(mBinding.getRoot());
|
||||
|
||||
initView();
|
||||
checkBluetooth();
|
||||
initData();
|
||||
|
||||
// 设置广播信息过滤
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(BluetoothDevice.ACTION_FOUND);//每搜索到一个设备就会发送一个该广播
|
||||
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//当全部搜索完后发送该广播
|
||||
filter.setPriority(Integer.MAX_VALUE);//设置优先级
|
||||
// 注册蓝牙搜索广播接收者,接收并处理搜索结果
|
||||
this.registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
bluetoothDevices.clear();
|
||||
Set<BluetoothDevice> bondedDevices = BtManager.INSTANCE.getBoundDevices();
|
||||
bluetoothDevices.addAll(bondedDevices);
|
||||
|
||||
btDeviceListAdapter = new BtDeviceListAdapter(this, bluetoothDevices);
|
||||
LinearLayoutManager manager = new LinearLayoutManager(getApplicationContext());
|
||||
mBinding.bleRv.setLayoutManager(manager);
|
||||
mBinding.bleRv.setAdapter(btDeviceListAdapter);
|
||||
|
||||
btDeviceListAdapter.setOnDeviceClickListener(
|
||||
index ->
|
||||
{
|
||||
BtManager.INSTANCE.connect(bluetoothDevices.get(index).getAddress());
|
||||
BtManager.INSTANCE.setClientListener(new BLEClientListener() {
|
||||
@Override
|
||||
public void onResult(@NonNull CommonMsg result) {
|
||||
Log.e("wangym", "onResult = " + result.getMsg());
|
||||
runOnUiThread(
|
||||
() -> Toast.makeText(BtDemoActivity.this, result.getMsg(),
|
||||
Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotifyMsgReceive(@NonNull byte[] msg) {
|
||||
Log.e("wangym", "notify = " + new String(msg));
|
||||
runOnUiThread(
|
||||
() -> Toast.makeText(BtDemoActivity.this, new String(msg),
|
||||
Toast.LENGTH_SHORT).show());
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
mBinding.searchBle.setOnClickListener(
|
||||
l -> BtManager.INSTANCE.searchBtDevice(BtConstants.BLUETOOTH_TYPE.CLASSIC));
|
||||
|
||||
mBinding.sendMsg.setOnClickListener(l -> {
|
||||
String value = "*" + mBinding.msgEt.getText().toString() + "#";
|
||||
BtManager.INSTANCE.writeMsg(value);
|
||||
});
|
||||
|
||||
mBinding.buildBleServer.setOnClickListener(v -> {
|
||||
BtManager.INSTANCE.initServer(this, BtConstants.BLUETOOTH_TYPE.BLE);
|
||||
BtManager.INSTANCE.setMsgReceiverListener(msg -> runOnUiThread(() -> {
|
||||
Object event = BtUtils.INSTANCE.getEventFromSendMsg(msg);
|
||||
if (event == null) {
|
||||
mBinding.receiveContent.setText(new String(msg));
|
||||
} else if (event instanceof KeyboardEvent) {
|
||||
KeyboardEvent temp = (KeyboardEvent) event;
|
||||
mBinding.receiveContent.setText(new Gson().toJson(temp));
|
||||
} else if (event instanceof AssociateEvent) {
|
||||
AssociateEvent temp = (AssociateEvent) event;
|
||||
mBinding.receiveContent.setText(new Gson().toJson(temp));
|
||||
} else if (event instanceof AssociateSourceEvent) {
|
||||
AssociateSourceEvent temp = (AssociateSourceEvent) event;
|
||||
mBinding.receiveContent.setText(new Gson().toJson(temp));
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private void checkBluetooth() {
|
||||
BtManager.INSTANCE.initClient(this, BtConstants.BLUETOOTH_TYPE.BLE);
|
||||
BtManager.INSTANCE.btPermissionCheck(this, PERMISSION_REQUEST_LOCATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 蓝牙扫描事件接收
|
||||
*/
|
||||
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
Log.d(TAG, "action = " + action);
|
||||
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
|
||||
// Discovery has found a device. Get the BluetoothDevice
|
||||
// object and its info from the Intent.
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
bluetoothDevices.add(device);
|
||||
btDeviceListAdapter.notifyDataSetChanged();
|
||||
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {//说明搜索已经完成
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_ENABLE_BT) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
Toast.makeText(this, "蓝牙打开成功", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(this, "蓝牙打开失败", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
switch (requestCode) {
|
||||
case PERMISSION_REQUEST_LOCATION:
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
|
||||
finish();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
BtManager.INSTANCE.release(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.common.bluetooth.bean
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* 联想词
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-11-1
|
||||
*/
|
||||
@Parcelize
|
||||
data class AssociateItem(
|
||||
/**
|
||||
* 联想词
|
||||
*/
|
||||
var name: String = "",
|
||||
/**
|
||||
* 序列号
|
||||
*/
|
||||
var index: Int = 0
|
||||
) : Parcelable
|
@ -0,0 +1,18 @@
|
||||
package com.common.bluetooth.bean
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* 待联想词事件
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-11-1
|
||||
*/
|
||||
@Parcelize
|
||||
data class AssociateSourceEvent(
|
||||
/**
|
||||
* 待联想词
|
||||
*/
|
||||
var associateSource: String = ""
|
||||
) : Parcelable
|
@ -0,0 +1,19 @@
|
||||
package com.common.bluetooth.bean
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* 基础事件类型
|
||||
*/
|
||||
@Parcelize
|
||||
data class BaseEvent(
|
||||
/**
|
||||
* 事件类型
|
||||
*/
|
||||
var type: String = "",
|
||||
/**
|
||||
* 事件内容
|
||||
*/
|
||||
var data: String = ""
|
||||
) : Parcelable
|
@ -0,0 +1,21 @@
|
||||
package com.common.bluetooth.bean
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
* 各类消息
|
||||
* @author wangym
|
||||
* @since 2021-11-1
|
||||
*/
|
||||
@Parcelize
|
||||
data class CommonMsg(
|
||||
/**
|
||||
* 消息类型
|
||||
*/
|
||||
var msgType: Int,
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
var msg: String
|
||||
) : Parcelable
|
@ -0,0 +1,19 @@
|
||||
package com.common.bluetooth.callback
|
||||
|
||||
import com.common.bluetooth.bean.CommonMsg
|
||||
|
||||
/**
|
||||
* 客户端监听
|
||||
*/
|
||||
interface BLEClientListener {
|
||||
/**
|
||||
* 结果
|
||||
* @param result 回调
|
||||
*/
|
||||
fun onResult(result: CommonMsg)
|
||||
|
||||
/**
|
||||
* 接收服务端通知
|
||||
*/
|
||||
fun onNotifyMsgReceive(msg: ByteArray)
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package com.common.bluetooth.callback;
|
||||
|
||||
/**
|
||||
* 用于数据回传的基本回调接口
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
public interface BaseResultCallback<T> {
|
||||
|
||||
/**
|
||||
* 成功拿到数据
|
||||
*
|
||||
* @param data 回传的数据
|
||||
*/
|
||||
void onSuccess(T data);
|
||||
|
||||
/**
|
||||
* 操作失败
|
||||
*
|
||||
* @param msg 失败的返回的异常信息
|
||||
*/
|
||||
void onFail(String msg);
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.common.bluetooth.callback
|
||||
|
||||
/**
|
||||
* BLE数据接收监听
|
||||
*/
|
||||
interface BleMsgReceiverListener {
|
||||
/**
|
||||
* 接收的消息
|
||||
*
|
||||
* @param msg 传输的数据
|
||||
*/
|
||||
fun onMsgReceive(msg: ByteArray)
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.common.bluetooth.callback
|
||||
|
||||
/**
|
||||
* 蓝牙链接监听
|
||||
* @author wangym
|
||||
* @since 2021-11-11
|
||||
*/
|
||||
interface BtConnectListener {
|
||||
/**
|
||||
* 链接成功
|
||||
*/
|
||||
fun onSuccess()
|
||||
|
||||
/**
|
||||
* 链接失败
|
||||
*
|
||||
* @param error 错误信息
|
||||
*/
|
||||
fun onFail(error: String)
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.common.bluetooth.callback
|
||||
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanResult
|
||||
|
||||
/**
|
||||
* 扫描结果回调
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-11-9
|
||||
*/
|
||||
open class BtScanCallBack : ScanCallback() {
|
||||
/**
|
||||
* 搜索结束
|
||||
*/
|
||||
open fun onComplete() {}
|
||||
|
||||
/**
|
||||
* 搜索报错
|
||||
* @param msg 错误信息
|
||||
*/
|
||||
open fun onError(msg: String?) {}
|
||||
|
||||
/**
|
||||
* 搜索结果
|
||||
* @param result 搜索结果
|
||||
*/
|
||||
open fun onResult(result: ScanResult?) {}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.common.bluetooth.exception;
|
||||
|
||||
/**
|
||||
* 蓝牙基础异常
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
public class BluetoothException extends Exception {
|
||||
public BluetoothException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.common.bluetooth.exception;
|
||||
|
||||
/**
|
||||
* 蓝牙未开启异常
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
public class BluetoothNotOpenException extends BluetoothException {
|
||||
public BluetoothNotOpenException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.common.bluetooth.exception;
|
||||
|
||||
/**
|
||||
* 蓝牙读异常
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
public class BluetoothReadException extends BluetoothException {
|
||||
public BluetoothReadException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.common.bluetooth.exception;
|
||||
|
||||
/**
|
||||
* 蓝牙搜索冲突异常
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
public class BluetoothSearchConflictException extends BluetoothException {
|
||||
public BluetoothSearchConflictException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.common.bluetooth.exception;
|
||||
|
||||
/**
|
||||
* 蓝牙写异常
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
public class BluetoothWriteException extends BluetoothException {
|
||||
private String mac = "";
|
||||
|
||||
public BluetoothWriteException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public BluetoothWriteException(String msg, String mac) {
|
||||
super(msg);
|
||||
this.mac = mac;
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package com.common.bluetooth.interfaces
|
||||
|
||||
/**
|
||||
* 蓝牙连接接口
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-11-9
|
||||
*/
|
||||
interface IBluetoothConnect {
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.common.bluetooth.interfaces
|
||||
|
||||
import com.common.bluetooth.callback.BtScanCallBack
|
||||
|
||||
/**
|
||||
* 蓝牙搜索接口
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
interface IBluetoothSearch {
|
||||
/**
|
||||
* 启动扫描
|
||||
*
|
||||
* 指定开始扫描蓝牙服务. 如果一个扫描服务正在运行,
|
||||
* 马上停止当前的扫描服务, 只进行新的扫描服务.
|
||||
*/
|
||||
fun startScan(delayTime: Long, callBack: BtScanCallBack?)
|
||||
|
||||
/**
|
||||
* 停止扫描
|
||||
*/
|
||||
fun stopScan()
|
||||
|
||||
/**
|
||||
* 是否正在扫描
|
||||
*
|
||||
* @return 是否启动扫描成功
|
||||
*/
|
||||
fun isScanning(): Boolean
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
package com.common.bluetooth.service;
|
||||
|
||||
import static com.common.bluetooth.BtConstants.CONNECT_SUCCESS;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.common.bluetooth.BtConstants;
|
||||
import com.common.bluetooth.BtUtils;
|
||||
import com.common.bluetooth.adapter.BluetoothClientBLEAdapter;
|
||||
import com.common.bluetooth.bean.CommonMsg;
|
||||
import com.common.bluetooth.callback.BLEClientListener;
|
||||
import com.common.bluetooth.callback.BaseResultCallback;
|
||||
import com.common.bluetooth.callback.BtConnectListener;
|
||||
import com.common.bluetooth.interfaces.IBluetoothClient;
|
||||
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
||||
/**
|
||||
* BLE客户端服务
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-29
|
||||
*/
|
||||
public class BLEClientService extends Service {
|
||||
private static final String TAG = "BLEClientService";
|
||||
|
||||
/**
|
||||
* 空闲
|
||||
*/
|
||||
private static final int WRITE_STATUS_IDLE = 1;
|
||||
/**
|
||||
* 传输中
|
||||
*/
|
||||
private static final int WRITE_STATUS_WRITING = 2;
|
||||
|
||||
/**
|
||||
* BLE客户端
|
||||
*/
|
||||
private IBluetoothClient mBtClient;
|
||||
|
||||
/**
|
||||
* 连接的MAC
|
||||
*/
|
||||
private String connectMac;
|
||||
|
||||
/**
|
||||
* 客户端事件监听器
|
||||
*/
|
||||
private BLEClientListener clientListener;
|
||||
|
||||
/**
|
||||
* 消息队列
|
||||
*/
|
||||
private final LinkedBlockingQueue<byte[]> msgQueue = new LinkedBlockingQueue<>();
|
||||
|
||||
/**
|
||||
* 是否开启通知
|
||||
*/
|
||||
private final AtomicBoolean isEnableNotify = new AtomicBoolean(true);
|
||||
|
||||
/**
|
||||
* 传输状态
|
||||
*/
|
||||
private final AtomicInteger writeStatus = new AtomicInteger(WRITE_STATUS_IDLE);
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
mBtClient = new BluetoothClientBLEAdapter(BluetoothLeClient.getInstance(this));
|
||||
mBtClient.checkBluetoothDevice(BtConstants.BLUETOOTH_TYPE.BLE);
|
||||
mBtClient.openBluetooth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接服务端设备
|
||||
*
|
||||
* @param mac MAC地址
|
||||
* @param enableNotify 是否注册通知
|
||||
*/
|
||||
public void connect(String mac, boolean enableNotify) {
|
||||
connect(mac, enableNotify, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知回调
|
||||
*/
|
||||
private final BaseResultCallback<byte[]> notifyCallback = new BaseResultCallback<byte[]>() {
|
||||
@Override
|
||||
public void onSuccess(byte[] data) {
|
||||
Log.d(TAG, "got notify onSuccess = " + new String(data));
|
||||
if (clientListener != null) {
|
||||
clientListener.onNotifyMsgReceive(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(String msg) {
|
||||
Log.d(TAG, "got notify onFail = " + msg);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 连接服务端设备
|
||||
*
|
||||
* @param mac MAC地址
|
||||
* @param enableNotify 是否注册通知
|
||||
* @param connectListener 链接回调
|
||||
*/
|
||||
public void connect(String mac, boolean enableNotify, BtConnectListener connectListener) {
|
||||
this.connectMac = mac;
|
||||
isEnableNotify.set(enableNotify);
|
||||
if (TextUtils.isEmpty(connectMac)) {
|
||||
connectMac = BtUtils.INSTANCE.getConnectMac(BLEClientService.this);
|
||||
}
|
||||
Observable<String>[] observables = new Observable[2];
|
||||
observables[0] = mBtClient.connect(connectMac);
|
||||
// 判断是否需要启动通知
|
||||
if (enableNotify) {
|
||||
observables[1] = mBtClient.registerNotify(connectMac, BtConstants.INSTANCE.getUUID_SERVICE(),
|
||||
BtConstants.INSTANCE.getUUID_CHARACTERISTIC_NOTIFY(), notifyCallback);
|
||||
}
|
||||
Observable.concatArray(observables).subscribe(new Observer<String>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Disposable d) {
|
||||
Log.d(TAG, "connect onSubscribe");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(@NonNull String value) {
|
||||
Log.d(TAG, String.format("connect onNext: %s", value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable e) {
|
||||
Log.e(TAG, "connect onError: ", e);
|
||||
// 链接过程报错,断开链接
|
||||
mBtClient.disconnect(connectMac);
|
||||
connectMac = "";
|
||||
|
||||
if (clientListener != null) {
|
||||
clientListener.onResult(new CommonMsg(BtConstants.CONNECT_ERROR, e.getMessage()));
|
||||
}
|
||||
if (connectListener != null) {
|
||||
connectListener.onFail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
Log.d(TAG, "connect onComplete");
|
||||
// 将链接成功的MAC地址保存
|
||||
BtUtils.INSTANCE.saveConnectMac(BLEClientService.this, mac);
|
||||
if (clientListener != null) {
|
||||
clientListener.onResult(new CommonMsg(CONNECT_SUCCESS, mac));
|
||||
}
|
||||
if (connectListener != null) {
|
||||
connectListener.onSuccess();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 传输内容
|
||||
*
|
||||
* @param msg 内容
|
||||
*/
|
||||
public void write(@NonNull final byte[] msg) {
|
||||
if (mBtClient == null) {
|
||||
Log.e(TAG, "write msg mBtClient got null");
|
||||
return;
|
||||
}
|
||||
if (TextUtils.isEmpty(connectMac)) {
|
||||
connectMac = BtUtils.INSTANCE.getConnectMac(BLEClientService.this);
|
||||
if (TextUtils.isEmpty(connectMac)) {
|
||||
Log.e(TAG, "write msg connectMac got null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "add msg to queue : " + new String(msg));
|
||||
|
||||
// 将字符串添加到队列中
|
||||
msgQueue.add(msg);
|
||||
doTranslate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行数据传输
|
||||
*/
|
||||
private void doTranslate() {
|
||||
// 判断状态
|
||||
if (writeStatus.get() == WRITE_STATUS_WRITING) {
|
||||
Log.d(TAG, "doTranslate is writing, return");
|
||||
return;
|
||||
}
|
||||
synchronized (BLEClientService.class) {
|
||||
writeStatus.set(WRITE_STATUS_WRITING);
|
||||
// 从队列中获取数据
|
||||
final byte[] msg = msgQueue.poll();
|
||||
if (msg == null) {
|
||||
Log.d(TAG, "doTranslate Queue is null");
|
||||
writeStatus.set(WRITE_STATUS_IDLE);
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "doTranslate start translate : " + new String(msg));
|
||||
// 检查需要发送的内容是否超过了限额
|
||||
int size = msg.length / BtConstants.MAX_MTU;
|
||||
if (msg.length % BtConstants.MAX_MTU != 0) {
|
||||
size++;
|
||||
}
|
||||
Observable<String>[] observables = new Observable[size];
|
||||
for (int index = 0; index < size; index++) {
|
||||
byte[] temp;
|
||||
int arrSize;
|
||||
if (index == size - 1) {
|
||||
arrSize = msg.length - (index * BtConstants.MAX_MTU);
|
||||
} else {
|
||||
arrSize = BtConstants.MAX_MTU;
|
||||
}
|
||||
temp = new byte[arrSize];
|
||||
System.arraycopy(msg, index * BtConstants.MAX_MTU, temp, 0, arrSize);
|
||||
Observable<String> observable = mBtClient.write(connectMac, BtConstants.INSTANCE.getUUID_SERVICE(),
|
||||
BtConstants.INSTANCE.getUUID_CHARACTERISTIC_WRITE(),
|
||||
temp);
|
||||
observables[index] = observable;
|
||||
}
|
||||
|
||||
Observable.concatArray(observables).subscribe(new Observer<String>() {
|
||||
@Override
|
||||
public void onSubscribe(@NonNull Disposable d) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(@NonNull String s) {
|
||||
Log.d(TAG, String.format("write onNext %s", s));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@NonNull Throwable e) {
|
||||
Log.e(TAG, "write onError: " + e.getMessage());
|
||||
if (BtConstants.EXCEPTION.NOT_CONNECTED.name().equals(e.getMessage())) {
|
||||
// 写时发现,链接断开,则尝试重连
|
||||
connect(connectMac, isEnableNotify.get(), new BtConnectListener() {
|
||||
@Override
|
||||
public void onSuccess() {
|
||||
// 将数据重新放入队列后再次传输
|
||||
msgQueue.add(msg);
|
||||
|
||||
writeStatus.set(WRITE_STATUS_IDLE);
|
||||
doTranslate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFail(@NonNull String error) {
|
||||
if (clientListener != null) {
|
||||
clientListener.onResult(new CommonMsg(BtConstants.WRITE_ERROR, error));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (clientListener != null) {
|
||||
clientListener.onResult(new CommonMsg(BtConstants.WRITE_ERROR, e.getMessage()));
|
||||
}
|
||||
}
|
||||
writeStatus.set(WRITE_STATUS_IDLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
Log.d(TAG, "write onComplete");
|
||||
if (clientListener != null) {
|
||||
clientListener.onResult(new CommonMsg(BtConstants.WRITE_SUCCESS, "write success"));
|
||||
}
|
||||
writeStatus.set(WRITE_STATUS_IDLE);
|
||||
Log.d(TAG, "doTranslate end translate : " + msg);
|
||||
doTranslate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void setClientListener(BLEClientListener clientListener) {
|
||||
this.clientListener = clientListener;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return new BLEClientBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
if (mBtClient != null && !TextUtils.isEmpty(connectMac)) {
|
||||
mBtClient.disconnect(connectMac);
|
||||
}
|
||||
return super.onUnbind(intent);
|
||||
}
|
||||
|
||||
public class BLEClientBinder extends Binder {
|
||||
public BLEClientService getService() {
|
||||
return BLEClientService.this;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package com.common.bluetooth.service;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.common.bluetooth.callback.BtScanCallBack;
|
||||
import com.common.bluetooth.interfaces.IBluetoothSearch;
|
||||
|
||||
/**
|
||||
* 经典蓝牙搜索
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
public class BluetoothClassicSearcher implements IBluetoothSearch {
|
||||
private static final String TAG = "BluetoothClassicSearcher";
|
||||
private final Context mContext;
|
||||
private final BluetoothAdapter mBluetoothAdapter;
|
||||
|
||||
public BluetoothClassicSearcher(Context mContext, BluetoothAdapter mBluetoothAdapter) {
|
||||
this.mContext = mContext;
|
||||
this.mBluetoothAdapter = mBluetoothAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopScan() {
|
||||
if (mBluetoothAdapter.isDiscovering()) {
|
||||
mBluetoothAdapter.cancelDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScanning() {
|
||||
return mBluetoothAdapter.isDiscovering();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScan(long delayTime, @Nullable BtScanCallBack callBack) {
|
||||
// 每次启动前,检查一下蓝牙是否已经在扫描之中
|
||||
stopScan();
|
||||
mBluetoothAdapter.startDiscovery();
|
||||
}
|
||||
}
|
@ -0,0 +1,519 @@
|
||||
/*
|
||||
* Copyright (C) 2014 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.common.bluetooth.service;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothServerSocket;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import com.common.bluetooth.BtConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* This class does all the work for setting up and managing Bluetooth
|
||||
* connections with other devices. It has a thread that listens for
|
||||
* incoming connections, a thread for connecting with a device, and a
|
||||
* thread for performing data transmissions when connected.
|
||||
*/
|
||||
public class BluetoothConnectService {
|
||||
// Debugging
|
||||
private static final String TAG = "BluetoothConnectService";
|
||||
|
||||
// Name for the SDP record when creating server socket
|
||||
private static final String NAME_SECURE = "BluetoothChatSecure";
|
||||
private static final String NAME_INSECURE = "BluetoothChatInsecure";
|
||||
|
||||
// Unique UUID for this application
|
||||
private final UUID CURRENT_UUID;
|
||||
|
||||
// Member fields
|
||||
private final BluetoothAdapter mAdapter;
|
||||
private final Handler mHandler;
|
||||
private AcceptThread mSecureAcceptThread;
|
||||
private AcceptThread mInsecureAcceptThread;
|
||||
private ConnectThread mConnectThread;
|
||||
private ConnectedThread mConnectedThread;
|
||||
private int mState;
|
||||
private int mNewState;
|
||||
|
||||
// Constants that indicate the current connection state
|
||||
public static final int STATE_NONE = 0; // we're doing nothing
|
||||
public static final int STATE_LISTEN = 1; // now listening for incoming connections
|
||||
public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
|
||||
public static final int STATE_CONNECTED = 3; // now connected to a remote device
|
||||
|
||||
/**
|
||||
* Constructor. Prepares a new BluetoothChat session.
|
||||
*
|
||||
* @param context The UI Activity Context
|
||||
* @param handler A Handler to send messages back to the UI Activity
|
||||
*/
|
||||
public BluetoothConnectService(Context context, Handler handler) {
|
||||
CURRENT_UUID = BtConstants.INSTANCE.getUUid(context);
|
||||
mAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
mState = STATE_NONE;
|
||||
mNewState = mState;
|
||||
mHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI title according to the current state of the chat connection
|
||||
*/
|
||||
private synchronized void updateUserInterfaceTitle() {
|
||||
mState = getState();
|
||||
Log.d(TAG, "updateUserInterfaceTitle() " + mNewState + " -> " + mState);
|
||||
mNewState = mState;
|
||||
|
||||
// Give the new state to the Handler so the UI Activity can update
|
||||
mHandler.obtainMessage(BtConstants.MESSAGE_STATE_CHANGE, mNewState, -1).sendToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current connection state.
|
||||
*/
|
||||
public synchronized int getState() {
|
||||
return mState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the chat service. Specifically start AcceptThread to begin a
|
||||
* session in listening (server) mode. Called by the Activity onResume()
|
||||
*/
|
||||
public synchronized void start() {
|
||||
Log.d(TAG, "start");
|
||||
|
||||
// Cancel any thread attempting to make a connection
|
||||
if (mConnectThread != null) {
|
||||
mConnectThread.cancel();
|
||||
mConnectThread = null;
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (mConnectedThread != null) {
|
||||
mConnectedThread.cancel();
|
||||
mConnectedThread = null;
|
||||
}
|
||||
|
||||
// Start the thread to listen on a BluetoothServerSocket
|
||||
if (mSecureAcceptThread == null) {
|
||||
mSecureAcceptThread = new AcceptThread(false);
|
||||
mSecureAcceptThread.start();
|
||||
}
|
||||
if (mInsecureAcceptThread == null) {
|
||||
mInsecureAcceptThread = new AcceptThread(false);
|
||||
mInsecureAcceptThread.start();
|
||||
}
|
||||
// Update UI title
|
||||
updateUserInterfaceTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the ConnectThread to initiate a connection to a remote device.
|
||||
*
|
||||
* @param device The BluetoothDevice to connect
|
||||
*/
|
||||
public synchronized void connect(BluetoothDevice device) {
|
||||
Log.d(TAG, "connect to: " + device);
|
||||
|
||||
// Cancel any thread attempting to make a connection
|
||||
if (mState == STATE_CONNECTING) {
|
||||
if (mConnectThread != null) {
|
||||
mConnectThread.cancel();
|
||||
mConnectThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (mConnectedThread != null) {
|
||||
mConnectedThread.cancel();
|
||||
mConnectedThread = null;
|
||||
}
|
||||
|
||||
// Start the thread to connect with the given device
|
||||
mConnectThread = new ConnectThread(device, false);
|
||||
mConnectThread.start();
|
||||
// Update UI title
|
||||
updateUserInterfaceTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the ConnectedThread to begin managing a Bluetooth connection
|
||||
*
|
||||
* @param socket The BluetoothSocket on which the connection was made
|
||||
* @param device The BluetoothDevice that has been connected
|
||||
*/
|
||||
public synchronized void connected(BluetoothSocket socket, BluetoothDevice
|
||||
device, final String socketType) {
|
||||
Log.d(TAG, "connected, Socket Type:" + socketType);
|
||||
|
||||
// Cancel the thread that completed the connection
|
||||
if (mConnectThread != null) {
|
||||
mConnectThread.cancel();
|
||||
mConnectThread = null;
|
||||
}
|
||||
|
||||
// Cancel any thread currently running a connection
|
||||
if (mConnectedThread != null) {
|
||||
mConnectedThread.cancel();
|
||||
mConnectedThread = null;
|
||||
}
|
||||
|
||||
// Cancel the accept thread because we only want to connect to one device
|
||||
if (mSecureAcceptThread != null) {
|
||||
mSecureAcceptThread.cancel();
|
||||
mSecureAcceptThread = null;
|
||||
}
|
||||
if (mInsecureAcceptThread != null) {
|
||||
mInsecureAcceptThread.cancel();
|
||||
mInsecureAcceptThread = null;
|
||||
}
|
||||
|
||||
// Start the thread to manage the connection and perform transmissions
|
||||
mConnectedThread = new ConnectedThread(socket, socketType);
|
||||
mConnectedThread.start();
|
||||
|
||||
// Send the name of the connected device back to the UI Activity
|
||||
Message msg = mHandler.obtainMessage(BtConstants.MESSAGE_DEVICE_NAME);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(BtConstants.DEVICE_NAME, device.getName());
|
||||
msg.setData(bundle);
|
||||
mHandler.sendMessage(msg);
|
||||
// Update UI title
|
||||
updateUserInterfaceTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all threads
|
||||
*/
|
||||
public synchronized void stop() {
|
||||
Log.d(TAG, "stop");
|
||||
|
||||
if (mConnectThread != null) {
|
||||
mConnectThread.cancel();
|
||||
mConnectThread = null;
|
||||
}
|
||||
|
||||
if (mConnectedThread != null) {
|
||||
mConnectedThread.cancel();
|
||||
mConnectedThread = null;
|
||||
}
|
||||
|
||||
if (mSecureAcceptThread != null) {
|
||||
mSecureAcceptThread.cancel();
|
||||
mSecureAcceptThread = null;
|
||||
}
|
||||
|
||||
if (mInsecureAcceptThread != null) {
|
||||
mInsecureAcceptThread.cancel();
|
||||
mInsecureAcceptThread = null;
|
||||
}
|
||||
mState = STATE_NONE;
|
||||
// Update UI title
|
||||
updateUserInterfaceTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to the ConnectedThread in an unsynchronized manner
|
||||
*
|
||||
* @param out The bytes to write
|
||||
* @see ConnectedThread#write(byte[])
|
||||
*/
|
||||
public void write(byte[] out) {
|
||||
// Create temporary object
|
||||
ConnectedThread r;
|
||||
// Synchronize a copy of the ConnectedThread
|
||||
synchronized (this) {
|
||||
if (mState != STATE_CONNECTED) return;
|
||||
r = mConnectedThread;
|
||||
}
|
||||
// Perform the write unsynchronized
|
||||
r.write(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the connection attempt failed and notify the UI Activity.
|
||||
*/
|
||||
private void connectionFailed() {
|
||||
// Send a failure message back to the Activity
|
||||
Message msg = mHandler.obtainMessage(BtConstants.MESSAGE_TOAST);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(BtConstants.TOAST, "Unable to connect device");
|
||||
msg.setData(bundle);
|
||||
mHandler.sendMessage(msg);
|
||||
|
||||
mState = STATE_NONE;
|
||||
// Update UI title
|
||||
updateUserInterfaceTitle();
|
||||
|
||||
// Start the service over to restart listening mode
|
||||
BluetoothConnectService.this.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the connection was lost and notify the UI Activity.
|
||||
*/
|
||||
private void connectionLost() {
|
||||
// Send a failure message back to the Activity
|
||||
Message msg = mHandler.obtainMessage(BtConstants.MESSAGE_TOAST);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(BtConstants.TOAST, "Device connection was lost");
|
||||
msg.setData(bundle);
|
||||
mHandler.sendMessage(msg);
|
||||
|
||||
mState = STATE_NONE;
|
||||
// Update UI title
|
||||
updateUserInterfaceTitle();
|
||||
|
||||
// Start the service over to restart listening mode
|
||||
BluetoothConnectService.this.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* This thread runs while listening for incoming connections. It behaves
|
||||
* like a server-side client. It runs until a connection is accepted
|
||||
* (or until cancelled).
|
||||
*/
|
||||
private class AcceptThread extends Thread {
|
||||
// The local server socket
|
||||
private final BluetoothServerSocket mmServerSocket;
|
||||
private String mSocketType;
|
||||
|
||||
public AcceptThread(boolean secure) {
|
||||
BluetoothServerSocket tmp = null;
|
||||
mSocketType = secure ? "Secure" : "Insecure";
|
||||
|
||||
// Create a new listening server socket
|
||||
try {
|
||||
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME_SECURE,
|
||||
CURRENT_UUID);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Socket Type: " + mSocketType + "listen() failed", e);
|
||||
}
|
||||
mmServerSocket = tmp;
|
||||
mState = STATE_LISTEN;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Log.d(TAG, "Socket Type: " + mSocketType +
|
||||
"BEGIN mAcceptThread" + this);
|
||||
setName("AcceptThread" + mSocketType);
|
||||
|
||||
BluetoothSocket socket = null;
|
||||
|
||||
// Listen to the server socket if we're not connected
|
||||
while (mState != STATE_CONNECTED) {
|
||||
try {
|
||||
// This is a blocking call and will only return on a
|
||||
// successful connection or an exception
|
||||
socket = mmServerSocket.accept();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Socket Type: " + mSocketType + "accept() failed", e);
|
||||
break;
|
||||
}
|
||||
|
||||
// If a connection was accepted
|
||||
if (socket != null) {
|
||||
synchronized (BluetoothConnectService.this) {
|
||||
switch (mState) {
|
||||
case STATE_LISTEN:
|
||||
case STATE_CONNECTING:
|
||||
// Situation normal. Start the connected thread.
|
||||
connected(socket, socket.getRemoteDevice(),
|
||||
mSocketType);
|
||||
break;
|
||||
case STATE_NONE:
|
||||
case STATE_CONNECTED:
|
||||
// Either not ready or already connected. Terminate new socket.
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Could not close unwanted socket", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "END mAcceptThread, socket Type: " + mSocketType);
|
||||
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
Log.d(TAG, "Socket Type" + mSocketType + "cancel " + this);
|
||||
try {
|
||||
mmServerSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Socket Type" + mSocketType + "close() of server failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This thread runs while attempting to make an outgoing connection
|
||||
* with a device. It runs straight through; the connection either
|
||||
* succeeds or fails.
|
||||
*/
|
||||
private class ConnectThread extends Thread {
|
||||
private BluetoothSocket mmSocket;
|
||||
private final BluetoothDevice mmDevice;
|
||||
private String mSocketType;
|
||||
|
||||
public ConnectThread(BluetoothDevice device, boolean secure) {
|
||||
mmDevice = device;
|
||||
BluetoothSocket tmp = null;
|
||||
mSocketType = secure ? "Secure" : "Insecure";
|
||||
|
||||
// Get a BluetoothSocket for a connection with the
|
||||
// given BluetoothDevice
|
||||
try {
|
||||
tmp = device.createRfcommSocketToServiceRecord(
|
||||
CURRENT_UUID);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e);
|
||||
}
|
||||
mmSocket = tmp;
|
||||
mState = STATE_CONNECTING;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Log.i(TAG, "BEGIN mConnectThread SocketType:" + mSocketType);
|
||||
setName("ConnectThread" + mSocketType);
|
||||
|
||||
// Always cancel discovery because it will slow down a connection
|
||||
mAdapter.cancelDiscovery();
|
||||
|
||||
// Make a connection to the BluetoothSocket
|
||||
try {
|
||||
// This is a blocking call and will only return on a
|
||||
// successful connection or an exception
|
||||
mmSocket.connect();
|
||||
} catch (IOException e) {
|
||||
// Unable to connect; close the socket and return.
|
||||
Log.e(TAG, e.toString());
|
||||
try {
|
||||
mmSocket.close();
|
||||
} catch (IOException ie) {
|
||||
connectionFailed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the ConnectThread because we're done
|
||||
synchronized (BluetoothConnectService.this) {
|
||||
mConnectThread = null;
|
||||
}
|
||||
|
||||
// Start the connected thread
|
||||
connected(mmSocket, mmDevice, mSocketType);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
try {
|
||||
mmSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "close() of connect " + mSocketType + " socket failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This thread runs during a connection with a remote device.
|
||||
* It handles all incoming and outgoing transmissions.
|
||||
*/
|
||||
private class ConnectedThread extends Thread {
|
||||
private final BluetoothSocket mmSocket;
|
||||
private final InputStream mmInStream;
|
||||
private final OutputStream mmOutStream;
|
||||
|
||||
public ConnectedThread(BluetoothSocket socket, String socketType) {
|
||||
Log.d(TAG, "create ConnectedThread: " + socketType);
|
||||
mmSocket = socket;
|
||||
InputStream tmpIn = null;
|
||||
OutputStream tmpOut = null;
|
||||
|
||||
// Get the BluetoothSocket input and output streams
|
||||
try {
|
||||
tmpIn = socket.getInputStream();
|
||||
tmpOut = socket.getOutputStream();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "temp sockets not created", e);
|
||||
}
|
||||
|
||||
mmInStream = tmpIn;
|
||||
mmOutStream = tmpOut;
|
||||
mState = STATE_CONNECTED;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Log.i(TAG, "BEGIN mConnectedThread");
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytes;
|
||||
|
||||
// Keep listening to the InputStream while connected
|
||||
while (mState == STATE_CONNECTED) {
|
||||
try {
|
||||
// Read from the InputStream
|
||||
bytes = mmInStream.read(buffer);
|
||||
|
||||
// Send the obtained bytes to the UI Activity
|
||||
mHandler.obtainMessage(BtConstants.MESSAGE_READ, bytes, -1, buffer)
|
||||
.sendToTarget();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "disconnected", e);
|
||||
connectionLost();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to the connected OutStream.
|
||||
*
|
||||
* @param buffer The bytes to write
|
||||
*/
|
||||
public void write(byte[] buffer) {
|
||||
try {
|
||||
mmOutStream.write(buffer);
|
||||
|
||||
// Share the sent message back to the UI Activity
|
||||
mHandler.obtainMessage(BtConstants.MESSAGE_WRITE, -1, -1, buffer)
|
||||
.sendToTarget();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Exception during write", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
try {
|
||||
mmSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "close() of connect socket failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package com.common.bluetooth.service;
|
||||
|
||||
import android.Manifest;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.le.ScanResult;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.common.bluetooth.callback.BtScanCallBack;
|
||||
import com.common.bluetooth.interfaces.IBluetoothSearch;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 蓝牙扫描服务封装类
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-10-19
|
||||
*/
|
||||
public class BluetoothLeSearcher implements IBluetoothSearch {
|
||||
private static final String TAG = "BluetoothLeSearcher";
|
||||
|
||||
private final BluetoothAdapter mBluetoothAdapter;
|
||||
|
||||
private final Handler mHandler;
|
||||
|
||||
private final Handler mAlertHandler;
|
||||
|
||||
private BtScanCallBack mScanCallback;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
private AtomicBoolean mScanning = new AtomicBoolean(false);
|
||||
|
||||
BluetoothLeSearcher(Context context, BluetoothAdapter adapter, Handler worker) {
|
||||
mContext = context;
|
||||
mBluetoothAdapter = adapter;
|
||||
mHandler = worker;
|
||||
|
||||
HandlerThread thread = new HandlerThread("bluetooth searcher handler");
|
||||
thread.start();
|
||||
mAlertHandler = new Handler(thread.getLooper());
|
||||
}
|
||||
|
||||
private BtScanCallBack wrapCallBack(final BtScanCallBack callBack) {
|
||||
return new BtScanCallBack() {
|
||||
@Override
|
||||
public void onComplete() {
|
||||
runOn(callBack::onComplete);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String msg) {
|
||||
runOn(() -> callBack.onError(msg));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanResult(int callbackType, ScanResult result) {
|
||||
runOn(() -> callBack.onResult(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBatchScanResults(List<ScanResult> results) {
|
||||
super.onBatchScanResults(results);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止扫描
|
||||
*/
|
||||
protected void stopLeScan() {
|
||||
if (mScanning.get()) {
|
||||
mScanning.set(false);
|
||||
mScanCallback.onComplete();
|
||||
|
||||
mAlertHandler.removeCallbacksAndMessages(null);
|
||||
mBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
|
||||
|
||||
mScanCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止BLE设备扫描
|
||||
*/
|
||||
public void stopScan() {
|
||||
runOn(this::stopScan);
|
||||
}
|
||||
|
||||
private void runOn(Runnable runnable) {
|
||||
mHandler.post(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScan(long delayTime, @Nullable BtScanCallBack callBack) {
|
||||
runOn(() -> {
|
||||
int permissionCheck1 = ContextCompat.checkSelfPermission(mContext,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
|
||||
int permissionCheck2 = ContextCompat.checkSelfPermission(mContext,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
if ((permissionCheck1 | permissionCheck2) != PackageManager.PERMISSION_GRANTED
|
||||
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
String err = "Cannot have location permission";
|
||||
Log.e(TAG, err);
|
||||
callBack.onError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mScanning.get()) {
|
||||
stopLeScan();
|
||||
}
|
||||
|
||||
mScanCallback = wrapCallBack(callBack);
|
||||
|
||||
// Stops scanning after a pre-defined scan period.
|
||||
// 预先定义停止蓝牙扫描的时间(因为蓝牙扫描需要消耗较多的电量)
|
||||
mAlertHandler.removeCallbacksAndMessages(null);
|
||||
mAlertHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
stopLeScan();
|
||||
}
|
||||
}, delayTime);
|
||||
|
||||
mScanning.set(true);
|
||||
|
||||
// 定义一个回调接口供扫描结束处理
|
||||
// 指定扫描特定的支持service的蓝牙设备
|
||||
// call startLeScan(UUID[], BluetoothAdapter.LeScanCallback)
|
||||
// 可以使用rssi计算蓝牙设备的距离
|
||||
// 计算公式:
|
||||
// d = 10^((abs(RSSI) - A) / (10 * n))
|
||||
// 其中:
|
||||
// d - 计算所得距离
|
||||
// RSSI - 接收信号强度(负值)
|
||||
// A - 射端和接收端相隔1米时的信号强度
|
||||
// n - 环境衰减因子
|
||||
mBluetoothAdapter.getBluetoothLeScanner().startScan(mScanCallback);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isScanning() {
|
||||
return mScanning.get();
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package com.common.bluetooth.view;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.common.bluetooth.databinding.RvBtDeviceItemBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 连接设备列表
|
||||
*
|
||||
* @author wangym
|
||||
* @since 2021-11-2
|
||||
*/
|
||||
public class BtDeviceListAdapter extends RecyclerView.Adapter<BtDeviceListAdapter.BtViewHolder> {
|
||||
private static final String TAG = "BtDeviceListAdapter";
|
||||
private Context context;
|
||||
private ArrayList<BluetoothDevice> bluetoothDevices;
|
||||
private RvBtDeviceItemBinding mBinding;
|
||||
private OnDeviceClickListener onDeviceClickListener;
|
||||
|
||||
public BtDeviceListAdapter(Context context, ArrayList<BluetoothDevice> bluetoothDevices) {
|
||||
this.context = context;
|
||||
this.bluetoothDevices = bluetoothDevices;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BtViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
mBinding = RvBtDeviceItemBinding.inflate(LayoutInflater.from(context));
|
||||
return new BtViewHolder(mBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull BtViewHolder holder, int position) {
|
||||
final int currentIndex = position;
|
||||
BluetoothDevice device = bluetoothDevices.get(position);
|
||||
holder.binding.btDeviceNameTv.setText("Device Name:" + device.getName());
|
||||
holder.binding.btDeviceMacTv.setText("Device MAC:" + device.getAddress());
|
||||
holder.binding.btDeviceItemRl.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (onDeviceClickListener != null) {
|
||||
onDeviceClickListener.onClick(currentIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return bluetoothDevices == null ? 0 : bluetoothDevices.size();
|
||||
}
|
||||
|
||||
protected static class BtViewHolder extends RecyclerView.ViewHolder {
|
||||
public RvBtDeviceItemBinding binding;
|
||||
|
||||
public BtViewHolder(@NonNull RvBtDeviceItemBinding binding) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnDeviceClickListener(OnDeviceClickListener onDeviceClickListener) {
|
||||
this.onDeviceClickListener = onDeviceClickListener;
|
||||
}
|
||||
|
||||
public interface OnDeviceClickListener {
|
||||
void onClick(int index);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.common.bluetooth.BtDemoActivity">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/ble_rv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/search_ble"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="搜索蓝牙" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/build_ble_server"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="建立ble服务" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/msg_et"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="24dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/send_msg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="发送数据" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/receive_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="24dp"
|
||||
android:text="收到的消息:" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/receive_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp" />
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</RelativeLayout>
|
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/bt_device_item_rl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bt_device_name_tv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bt_device_mac_tv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/bt_device_name_tv"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="24dp" />
|
||||
</RelativeLayout>
|
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<color name="label_color">#ffffffff</color>
|
||||
<color name="label_color_hl0">#ffffffff</color>
|
||||
<color name="balloon_color">#ff000000</color>
|
||||
<color name="candidate_color">#ff000000</color>
|
||||
<color name="active_candidate_color">#ff000000</color>
|
||||
<color name="recommended_candidate_color">#ffe35900</color>
|
||||
<color name="footnote_color">#ff343233</color>
|
||||
<color name="composing_color">#ff000000</color>
|
||||
<color name="composing_color_hl">#ffffffff</color>
|
||||
<color name="composing_color_idle">#ff777777</color>
|
||||
</resources>
|
@ -1,3 +1,4 @@
|
||||
rootProject.name = "CommonLibTest"
|
||||
include ':app'
|
||||
include ':commonLib'
|
||||
include ':commonbt'
|
||||
|
Loading…
Reference in New Issue