diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 71777098..19fbd6a5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -67,6 +67,11 @@
+
+
+
+
+
+ tools:ignore="DiscouragedApi,TranslucentOrientation">
@@ -113,21 +118,24 @@
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:screenOrientation="portrait"
- android:windowSoftInputMode="adjustPan|stateHidden" />
+ android:windowSoftInputMode="adjustPan|stateHidden"
+ tools:ignore="DiscouragedApi" />
+ android:windowSoftInputMode="adjustPan|stateHidden"
+ tools:ignore="DiscouragedApi" />
+ android:windowSoftInputMode="adjustPan|stateHidden"
+ tools:ignore="DiscouragedApi" />
+ android:windowSoftInputMode="adjustPan|stateHidden"
+ tools:ignore="DiscouragedApi" />
+ android:theme="@style/DialogTheme"
+ tools:ignore="DiscouragedApi" />
+ android:theme="@style/DialogTheme"
+ tools:ignore="DiscouragedApi" />
+ android:windowSoftInputMode="adjustPan|stateHidden"
+ tools:ignore="DiscouragedApi" />
+
@@ -244,6 +260,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= Build.VERSION_CODES.O) {
startForegroundService(foregroundServiceIntent)
} else {
@@ -202,7 +207,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
//启动LocationService
if (SettingUtils.enableLocation) {
val locationServiceIntent = Intent(this, LocationService::class.java)
- locationServiceIntent.action = "START"
+ locationServiceIntent.action = ACTION_START
startService(locationServiceIntent)
}
@@ -211,6 +216,26 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
val batteryFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
registerReceiver(batteryReceiver, batteryFilter)
+ //监听蓝牙状态变化
+ val bluetoothReceiver = BluetoothReceiver()
+ val filter = IntentFilter().apply {
+ addAction(BluetoothDevice.ACTION_FOUND)
+ addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
+ addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
+ addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)
+ addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)
+ addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
+ addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
+ addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
+ addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
+ }
+ registerReceiver(bluetoothReceiver, filter)
+ if (SettingUtils.enableBluetooth) {
+ val bluetoothScanServiceIntent = Intent(this, BluetoothScanService::class.java)
+ bluetoothScanServiceIntent.action = ACTION_START
+ startService(bluetoothScanServiceIntent)
+ }
+
//监听网络变化
val networkReceiver = NetworkChangeReceiver()
val networkFilter = IntentFilter().apply {
diff --git a/app/src/main/java/com/idormy/sms/forwarder/activity/MainActivity.kt b/app/src/main/java/com/idormy/sms/forwarder/activity/MainActivity.kt
index 870d7323..9138a5a7 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/activity/MainActivity.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/activity/MainActivity.kt
@@ -36,6 +36,7 @@ import com.idormy.sms.forwarder.fragment.ServerFragment
import com.idormy.sms.forwarder.fragment.SettingsFragment
import com.idormy.sms.forwarder.fragment.TasksFragment
import com.idormy.sms.forwarder.service.ForegroundService
+import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.CommonUtils.Companion.restartApplication
import com.idormy.sms.forwarder.utils.EVENT_LOAD_APP_LIST
import com.idormy.sms.forwarder.utils.FRPC_LIB_DOWNLOAD_URL
@@ -121,7 +122,7 @@ class MainActivity : BaseActivity(), DrawerAdapter.OnItemS
//启动前台服务
if (!ForegroundService.isRunning) {
val serviceIntent = Intent(this, ForegroundService::class.java)
- serviceIntent.action = "START"
+ serviceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
diff --git a/app/src/main/java/com/idormy/sms/forwarder/adapter/BluetoothRecyclerAdapter.kt b/app/src/main/java/com/idormy/sms/forwarder/adapter/BluetoothRecyclerAdapter.kt
new file mode 100644
index 00000000..09aacbe7
--- /dev/null
+++ b/app/src/main/java/com/idormy/sms/forwarder/adapter/BluetoothRecyclerAdapter.kt
@@ -0,0 +1,122 @@
+package com.idormy.sms.forwarder.adapter
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothClass
+import android.bluetooth.BluetoothDevice
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.idormy.sms.forwarder.R
+
+class BluetoothRecyclerAdapter(
+ private val itemList: List,
+ private var itemClickListener: ((Int) -> Unit)? = null,
+ private var removeClickListener: ((Int) -> Unit)? = null,
+ private var editClickListener: ((Int) -> Unit)? = null,
+) : RecyclerView.Adapter() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_bluetooth_list_item, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = itemList[position]
+ holder.bind(item)
+ }
+
+ override fun getItemCount(): Int = itemList.size
+
+
+ @Suppress("DEPRECATION")
+ inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
+ private val textDeviceName: TextView = itemView.findViewById(R.id.text_device_name)
+ private val textDeviceAddress: TextView = itemView.findViewById(R.id.text_device_address)
+ private val imageDeviceIcon: ImageView = itemView.findViewById(R.id.image_device_icon)
+ private val editIcon: ImageView = itemView.findViewById(R.id.iv_edit)
+ private val removeIcon: ImageView = itemView.findViewById(R.id.iv_remove)
+
+ init {
+ if (removeClickListener == null) {
+ removeIcon.visibility = View.GONE
+ } else {
+ removeIcon.setOnClickListener(this)
+ }
+
+ if (editClickListener == null) {
+ editIcon.visibility = View.GONE
+ } else {
+ editIcon.setOnClickListener(this)
+ }
+
+ if (itemClickListener != null) {
+ itemView.setOnClickListener(this)
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ fun bind(device: BluetoothDevice) {
+ // 设置设备名称和地址
+ textDeviceName.text = device.name ?: "Unknown Device"
+ textDeviceAddress.text = device.address
+
+ // 根据设备类型设置图标
+ val deviceType = getDeviceType(device)
+ val iconResId = when (deviceType) {
+ DeviceType.CELLPHONE -> R.drawable.ic_bt_cellphone
+ DeviceType.HEADPHONES -> R.drawable.ic_bt_headphones
+ DeviceType.HEADSET_HFP -> R.drawable.ic_bt_headset_hfp
+ DeviceType.IMAGING -> R.drawable.ic_bt_imaging
+ DeviceType.LAPTOP -> R.drawable.ic_bt_laptop
+ DeviceType.MISC_HID -> R.drawable.ic_bt_misc_hid
+ DeviceType.NETWORK_PAN -> R.drawable.ic_bt_network_pan
+ DeviceType.WRISTBAND -> R.drawable.ic_bt_wristband
+ else -> R.drawable.ic_bt_bluetooth
+ }
+ imageDeviceIcon.setImageResource(iconResId)
+ }
+
+ override fun onClick(v: View?) {
+ val position = adapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ when (v?.id) {
+ R.id.iv_edit -> editClickListener?.let { it(position) }
+ R.id.iv_remove -> removeClickListener?.let { it(position) }
+ else -> itemClickListener?.let { it(position) }
+ }
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun getDeviceType(device: BluetoothDevice): DeviceType {
+ val deviceClass = device.bluetoothClass?.majorDeviceClass ?: BluetoothClass.Device.Major.MISC
+ @Suppress("DUPLICATE_LABEL_IN_WHEN")
+ return when (deviceClass) {
+ BluetoothClass.Device.Major.PHONE -> DeviceType.CELLPHONE
+ BluetoothClass.Device.Major.AUDIO_VIDEO -> DeviceType.HEADPHONES
+ BluetoothClass.Device.Major.PERIPHERAL -> DeviceType.HEADSET_HFP
+ BluetoothClass.Device.Major.IMAGING -> DeviceType.IMAGING
+ BluetoothClass.Device.Major.COMPUTER -> DeviceType.LAPTOP
+ BluetoothClass.Device.Major.PERIPHERAL -> DeviceType.MISC_HID
+ BluetoothClass.Device.Major.NETWORKING -> DeviceType.NETWORK_PAN
+ BluetoothClass.Device.Major.WEARABLE -> DeviceType.WRISTBAND
+ else -> DeviceType.UNKNOWN
+ }
+ }
+ }
+
+ enum class DeviceType {
+ CELLPHONE,
+ HEADPHONES,
+ HEADSET_HFP,
+ IMAGING,
+ LAPTOP,
+ MISC_HID,
+ NETWORK_PAN,
+ WRISTBAND,
+ UNKNOWN
+ }
+}
diff --git a/app/src/main/java/com/idormy/sms/forwarder/entity/condition/BluetoothSetting.kt b/app/src/main/java/com/idormy/sms/forwarder/entity/condition/BluetoothSetting.kt
new file mode 100644
index 00000000..b7c44832
--- /dev/null
+++ b/app/src/main/java/com/idormy/sms/forwarder/entity/condition/BluetoothSetting.kt
@@ -0,0 +1,88 @@
+package com.idormy.sms.forwarder.entity.condition
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import com.idormy.sms.forwarder.App
+import com.idormy.sms.forwarder.R
+import com.xuexiang.xutil.resource.ResUtils.getString
+import java.io.Serializable
+
+data class BluetoothSetting(
+ var description: String = "", //描述
+ var action: String = BluetoothAdapter.ACTION_STATE_CHANGED, //事件
+ var state: Int = BluetoothAdapter.STATE_ON, //蓝牙状态
+ var result: Int = 1, //搜索结果:1-已发现,0-未发现
+ var device: String = "", //设备MAC地址
+) : Serializable {
+
+ constructor(actionCheckId: Int, stateCheckId: Int, resultCheckId: Int, deviceAddress: String) : this() {
+ device = deviceAddress
+ action = when (actionCheckId) {
+ R.id.rb_action_discovery_finished -> BluetoothAdapter.ACTION_DISCOVERY_FINISHED
+ R.id.rb_action_acl_connected -> BluetoothDevice.ACTION_ACL_CONNECTED
+ R.id.rb_action_acl_disconnected -> BluetoothDevice.ACTION_ACL_DISCONNECTED
+ else -> BluetoothAdapter.ACTION_STATE_CHANGED
+ }
+ state = when (stateCheckId) {
+ R.id.rb_state_off -> BluetoothAdapter.STATE_OFF
+ else -> BluetoothAdapter.STATE_ON
+ }
+ result = when (resultCheckId) {
+ R.id.rb_undiscovered -> 0
+ else -> 1
+ }
+ val sb = StringBuilder()
+
+ if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
+ sb.append(getString(R.string.bluetooth_state_changed)).append(", ").append(getString(R.string.specified_state)).append(": ")
+ if (state == BluetoothAdapter.STATE_ON) {
+ sb.append(getString(R.string.state_on))
+ } else {
+ sb.append(getString(R.string.state_off))
+ }
+ } else if (action == BluetoothAdapter.ACTION_DISCOVERY_FINISHED) {
+ sb.append(getString(R.string.bluetooth_discovery_finished)).append(", ")
+ if (result == 1) {
+ sb.append(getString(R.string.discovered))
+ } else {
+ sb.append(getString(R.string.undiscovered))
+ }
+ val blank = if (App.isNeedSpaceBetweenWords) " " else ""
+ sb.append(blank).append(getString(R.string.specified_device)).append(": ").append(device)
+ } else {
+ if (action == BluetoothDevice.ACTION_ACL_CONNECTED) {
+ sb.append(getString(R.string.bluetooth_acl_connected))
+ } else if (action == BluetoothDevice.ACTION_ACL_DISCONNECTED) {
+ sb.append(getString(R.string.bluetooth_acl_disconnected))
+ }
+ sb.append(", ").append(getString(R.string.specified_device)).append(": ").append(device)
+ }
+
+ description = sb.toString()
+ }
+
+ fun getActionCheckId(): Int {
+ return when (action) {
+ BluetoothAdapter.ACTION_STATE_CHANGED -> R.id.rb_action_state_changed
+ BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> R.id.rb_action_discovery_finished
+ BluetoothDevice.ACTION_ACL_CONNECTED -> R.id.rb_action_acl_connected
+ BluetoothDevice.ACTION_ACL_DISCONNECTED -> R.id.rb_action_acl_disconnected
+ else -> R.id.rb_action_state_changed
+ }
+ }
+
+ fun getStateCheckId(): Int {
+ return when (state) {
+ BluetoothAdapter.STATE_ON -> R.id.rb_state_on
+ BluetoothAdapter.STATE_OFF -> R.id.rb_state_off
+ else -> R.id.rb_state_on
+ }
+ }
+
+ fun getResultCheckId(): Int {
+ return when (result) {
+ 0 -> R.id.rb_undiscovered
+ else -> R.id.rb_discovered
+ }
+ }
+}
diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/FrpcFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/FrpcFragment.kt
index e697e6a8..6b2f9a4f 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/fragment/FrpcFragment.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/FrpcFragment.kt
@@ -18,6 +18,7 @@ import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory
import com.idormy.sms.forwarder.database.viewmodel.FrpcViewModel
import com.idormy.sms.forwarder.databinding.FragmentFrpcsBinding
import com.idormy.sms.forwarder.service.ForegroundService
+import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.EVENT_FRPC_DELETE_CONFIG
import com.idormy.sms.forwarder.utils.EVENT_FRPC_RUNNING_ERROR
import com.idormy.sms.forwarder.utils.EVENT_FRPC_RUNNING_SUCCESS
@@ -153,7 +154,7 @@ class FrpcFragment : BaseFragment(), FrpcPagingAdapter.On
if (!ForegroundService.isRunning) {
val serviceIntent = Intent(requireContext(), ForegroundService::class.java)
- serviceIntent.action = "START"
+ serviceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requireContext().startForegroundService(serviceIntent)
} else {
diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/ServerFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/ServerFragment.kt
index c5a106e3..4333a187 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/fragment/ServerFragment.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/ServerFragment.kt
@@ -21,7 +21,15 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentServerBinding
import com.idormy.sms.forwarder.service.HttpServerService
import com.idormy.sms.forwarder.service.LocationService
-import com.idormy.sms.forwarder.utils.*
+import com.idormy.sms.forwarder.utils.ACTION_RESTART
+import com.idormy.sms.forwarder.utils.Base64
+import com.idormy.sms.forwarder.utils.HTTP_SERVER_PORT
+import com.idormy.sms.forwarder.utils.HttpServerUtils
+import com.idormy.sms.forwarder.utils.Log
+import com.idormy.sms.forwarder.utils.RandomUtils
+import com.idormy.sms.forwarder.utils.SM4Crypt
+import com.idormy.sms.forwarder.utils.SettingUtils
+import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xui.widget.actionbar.TitleBar
@@ -256,7 +264,7 @@ class ServerFragment : BaseFragment(), View.OnClickListe
}
//重启前台服务,启动/停止定位服务
val serviceIntent = Intent(requireContext(), LocationService::class.java)
- serviceIntent.action = "RESTART"
+ serviceIntent.action = ACTION_RESTART
requireContext().startService(serviceIntent)
}
diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/SettingsFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/SettingsFragment.kt
index 59e9ef15..e6422ee5 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/fragment/SettingsFragment.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/SettingsFragment.kt
@@ -42,12 +42,19 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentSettingsBinding
import com.idormy.sms.forwarder.entity.SimInfo
import com.idormy.sms.forwarder.receiver.BootCompletedReceiver
+import com.idormy.sms.forwarder.service.BluetoothScanService
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.service.LocationService
+import com.idormy.sms.forwarder.utils.ACTION_RESTART
+import com.idormy.sms.forwarder.utils.ACTION_START
+import com.idormy.sms.forwarder.utils.ACTION_STOP
+import com.idormy.sms.forwarder.utils.ACTION_UPDATE_NOTIFICATION
import com.idormy.sms.forwarder.utils.AppUtils.getAppPackageName
+import com.idormy.sms.forwarder.utils.BluetoothUtils
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.DataProvider
import com.idormy.sms.forwarder.utils.EVENT_LOAD_APP_LIST
+import com.idormy.sms.forwarder.utils.EXTRA_UPDATE_NOTIFICATION
import com.idormy.sms.forwarder.utils.KeepAliveUtils
import com.idormy.sms.forwarder.utils.LocationUtils
import com.idormy.sms.forwarder.utils.Log
@@ -124,7 +131,9 @@ class SettingsFragment : BaseFragment(), View.OnClickL
//转发应用通知
switchEnableAppNotify(binding!!.sbEnableAppNotify, binding!!.scbCancelAppNotify, binding!!.scbNotUserPresent)
- //启用GPS定位功能
+ //发现蓝牙设备服务
+ switchEnableBluetooth(binding!!.sbEnableBluetooth, binding!!.layoutBluetoothSetting, binding!!.xsbScanInterval, binding!!.scbIgnoreAnonymous)
+ //GPS定位功能
switchEnableLocation(binding!!.sbEnableLocation, binding!!.layoutLocationSetting, binding!!.rgAccuracy, binding!!.rgPowerRequirement, binding!!.xsbMinInterval, binding!!.xsbMinDistance)
//短信指令
switchEnableSmsCommand(binding!!.sbEnableSmsCommand, binding!!.etSafePhone)
@@ -309,10 +318,10 @@ class SettingsFragment : BaseFragment(), View.OnClickL
}
R.id.btn_export_log -> {
- // 申请储存权限
XXPermissions.with(this)
- //.permission(*Permission.Group.STORAGE)
- .permission(Permission.MANAGE_EXTERNAL_STORAGE).request(object : OnPermissionCallback {
+ // 申请储存权限
+ .permission(Permission.MANAGE_EXTERNAL_STORAGE)
+ .request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List, all: Boolean) {
try {
@@ -352,7 +361,6 @@ class SettingsFragment : BaseFragment(), View.OnClickL
sbEnableSms.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
SettingUtils.enableSms = isChecked
if (isChecked) {
- //检查权限是否获取
XXPermissions.with(this)
// 接收 WAP 推送消息
.permission(Permission.RECEIVE_WAP_PUSH)
@@ -408,7 +416,6 @@ class SettingsFragment : BaseFragment(), View.OnClickL
}
SettingUtils.enablePhone = isChecked
if (isChecked) {
- //检查权限是否获取
XXPermissions.with(this)
// 读取电话状态
.permission(Permission.READ_PHONE_STATE)
@@ -499,7 +506,6 @@ class SettingsFragment : BaseFragment(), View.OnClickL
binding!!.layoutOptionalAction.visibility = if (isChecked) View.VISIBLE else View.GONE
SettingUtils.enableAppNotify = isChecked
if (isChecked) {
- //检查权限是否获取
XXPermissions.with(this)
.permission(Permission.BIND_NOTIFICATION_LISTENER_SERVICE)
.request(OnPermissionCallback { permissions, allGranted ->
@@ -531,7 +537,85 @@ class SettingsFragment : BaseFragment(), View.OnClickL
}
}
- //启用定位功能
+ //发现蓝牙设备服务
+ private fun switchEnableBluetooth(@SuppressLint("UseSwitchCompatOrMaterialCode") sbEnableBluetooth: SwitchButton, layoutBluetoothSetting: LinearLayout, xsbScanInterval: XSeekBar, scbIgnoreAnonymous: SmoothCheckBox) {
+ sbEnableBluetooth.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
+ SettingUtils.enableBluetooth = isChecked
+ layoutBluetoothSetting.visibility = if (isChecked) View.VISIBLE else View.GONE
+ if (isChecked) {
+ XXPermissions.with(this)
+ .permission(Permission.BLUETOOTH_SCAN)
+ .permission(Permission.BLUETOOTH_CONNECT)
+ .permission(Permission.BLUETOOTH_ADVERTISE)
+ .permission(Permission.ACCESS_FINE_LOCATION)
+ .request(object : OnPermissionCallback {
+ override fun onGranted(permissions: List, all: Boolean) {
+ Log.d(TAG, "onGranted: permissions=$permissions, all=$all")
+ if (!all) {
+ XToastUtils.warning(getString(R.string.enable_bluetooth) + ": " + getString(R.string.toast_granted_part))
+ }
+ restartBluetoothService(ACTION_START)
+ }
+
+ override fun onDenied(permissions: List, never: Boolean) {
+ Log.e(TAG, "onDenied: permissions=$permissions, never=$never")
+ if (never) {
+ XToastUtils.error(getString(R.string.enable_bluetooth) + ": " + getString(R.string.toast_denied_never))
+ // 如果是被永久拒绝就跳转到应用权限系统设置页面
+ XXPermissions.startPermissionActivity(requireContext(), permissions)
+ } else {
+ XToastUtils.error(getString(R.string.enable_bluetooth) + ": " + getString(R.string.toast_denied))
+ }
+ SettingUtils.enableBluetooth = false
+ sbEnableBluetooth.isChecked = false
+ restartBluetoothService(ACTION_STOP)
+ }
+ })
+ } else {
+ restartBluetoothService(ACTION_STOP)
+ }
+ }
+ val isEnable = SettingUtils.enableBluetooth
+ sbEnableBluetooth.isChecked = isEnable
+ layoutBluetoothSetting.visibility = if (isEnable) View.VISIBLE else View.GONE
+
+ //扫描蓝牙设备间隔
+ xsbScanInterval.setDefaultValue((SettingUtils.bluetoothScanInterval / 1000).toInt())
+ xsbScanInterval.setOnSeekBarListener { _: XSeekBar?, newValue: Int ->
+ if (newValue * 1000L != SettingUtils.bluetoothScanInterval) {
+ SettingUtils.bluetoothScanInterval = newValue * 1000L
+ restartBluetoothService()
+ }
+ }
+
+ //是否忽略匿名设备
+ scbIgnoreAnonymous.isChecked = SettingUtils.bluetoothIgnoreAnonymous
+ scbIgnoreAnonymous.setOnCheckedChangeListener { _: SmoothCheckBox, isChecked: Boolean ->
+ SettingUtils.bluetoothIgnoreAnonymous = isChecked
+ restartBluetoothService()
+ }
+
+ }
+
+ //重启蓝牙扫描服务
+ private fun restartBluetoothService(action: String = ACTION_RESTART) {
+ if (!initViewsFinished) return
+ Log.d(TAG, "restartBluetoothService, action: $action")
+ val serviceIntent = Intent(requireContext(), BluetoothScanService::class.java)
+ //如果定位功能已启用,但是系统定位功能不可用,则关闭定位功能
+ if (SettingUtils.enableBluetooth && (!BluetoothUtils.isBluetoothEnabled() || !BluetoothUtils.hasBluetoothCapability(App.context))) {
+ XToastUtils.error(getString(R.string.toast_location_not_enabled))
+ SettingUtils.enableBluetooth = false
+ binding!!.sbEnableBluetooth.isChecked = false
+ binding!!.layoutBluetoothSetting.visibility = View.GONE
+ serviceIntent.action = ACTION_STOP
+ } else {
+ serviceIntent.action = action
+ }
+ requireContext().startService(serviceIntent)
+ }
+
+ //GPS定位服务
private fun switchEnableLocation(@SuppressLint("UseSwitchCompatOrMaterialCode") sbEnableLocation: SwitchButton, layoutLocationSetting: LinearLayout, rgAccuracy: RadioGroup, rgPowerRequirement: RadioGroup, xsbMinInterval: XSeekBar, xsbMinDistance: XSeekBar) {
sbEnableLocation.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
SettingUtils.enableLocation = isChecked
@@ -547,7 +631,7 @@ class SettingsFragment : BaseFragment(), View.OnClickL
if (!all) {
XToastUtils.warning(getString(R.string.enable_location) + ": " + getString(R.string.toast_granted_part))
}
- restartLocationService("START")
+ restartLocationService(ACTION_START)
}
override fun onDenied(permissions: List, never: Boolean) {
@@ -561,11 +645,11 @@ class SettingsFragment : BaseFragment(), View.OnClickL
}
SettingUtils.enableLocation = false
sbEnableLocation.isChecked = false
- restartLocationService("STOP")
+ restartLocationService(ACTION_STOP)
}
})
} else {
- restartLocationService("STOP")
+ restartLocationService(ACTION_STOP)
}
}
val isEnable = SettingUtils.enableLocation
@@ -632,7 +716,7 @@ class SettingsFragment : BaseFragment(), View.OnClickL
}
//重启定位服务
- private fun restartLocationService(action: String = "RESTART") {
+ private fun restartLocationService(action: String = ACTION_RESTART) {
if (!initViewsFinished) return
Log.d(TAG, "restartLocationService, action: $action")
val serviceIntent = Intent(requireContext(), LocationService::class.java)
@@ -642,7 +726,7 @@ class SettingsFragment : BaseFragment(), View.OnClickL
SettingUtils.enableLocation = false
binding!!.sbEnableLocation.isChecked = false
binding!!.layoutLocationSetting.visibility = View.GONE
- serviceIntent.action = "STOP"
+ serviceIntent.action = ACTION_STOP
} else {
serviceIntent.action = action
}
@@ -656,7 +740,6 @@ class SettingsFragment : BaseFragment(), View.OnClickL
SettingUtils.enableSmsCommand = isChecked
etSafePhone.visibility = if (isChecked) View.VISIBLE else View.GONE
if (isChecked) {
- //检查权限是否获取
XXPermissions.with(this)
// 系统设置
.permission(Permission.WRITE_SETTINGS)
@@ -962,8 +1045,8 @@ class SettingsFragment : BaseFragment(), View.OnClickL
val notifyContent = etNotifyContent.text.toString().trim()
SettingUtils.notifyContent = notifyContent
val updateIntent = Intent(context, ForegroundService::class.java)
- updateIntent.action = "UPDATE_NOTIFICATION"
- updateIntent.putExtra("UPDATED_CONTENT", notifyContent)
+ updateIntent.action = ACTION_UPDATE_NOTIFICATION
+ updateIntent.putExtra(EXTRA_UPDATE_NOTIFICATION, notifyContent)
context?.let { ContextCompat.startForegroundService(it, updateIntent) }
}
})
diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/TasksEditFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/TasksEditFragment.kt
index 2ec2f39f..17d45cea 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/fragment/TasksEditFragment.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/TasksEditFragment.kt
@@ -82,56 +82,56 @@ class TasksEditFragment : BaseFragment(), View.OnClic
PageInfo(
getString(R.string.task_cron),
"com.idormy.sms.forwarder.fragment.condition.CronFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_custom_time,
),
PageInfo(
getString(R.string.task_to_address),
"com.idormy.sms.forwarder.fragment.condition.ToAddressFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_to_address,
),
PageInfo(
getString(R.string.task_leave_address),
"com.idormy.sms.forwarder.fragment.condition.LeaveAddressFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_leave_address,
),
PageInfo(
getString(R.string.task_network),
"com.idormy.sms.forwarder.fragment.condition.NetworkFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_network
),
PageInfo(
getString(R.string.task_sim),
"com.idormy.sms.forwarder.fragment.condition.SimFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_sim
),
PageInfo(
getString(R.string.task_battery),
"com.idormy.sms.forwarder.fragment.condition.BatteryFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_battery
),
PageInfo(
getString(R.string.task_charge),
"com.idormy.sms.forwarder.fragment.condition.ChargeFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_charge
),
PageInfo(
getString(R.string.task_lock_screen),
"com.idormy.sms.forwarder.fragment.condition.LockScreenFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_lock_screen
),
@@ -156,83 +156,90 @@ class TasksEditFragment : BaseFragment(), View.OnClic
CoreAnim.slide,
R.drawable.auto_task_icon_start_activity
),
+ PageInfo(
+ getString(R.string.task_bluetooth),
+ "com.idormy.sms.forwarder.fragment.condition.BluetoothFragment",
+ "",
+ CoreAnim.slide,
+ R.drawable.auto_task_icon_bluetooth
+ ),
)
private var TASK_ACTION_FRAGMENT_LIST = listOf(
PageInfo(
getString(R.string.task_sendsms),
"com.idormy.sms.forwarder.fragment.action.SendSmsFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_sms
),
PageInfo(
getString(R.string.task_notification),
"com.idormy.sms.forwarder.fragment.action.NotificationFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_notification,
),
PageInfo(
getString(R.string.task_cleaner),
"com.idormy.sms.forwarder.fragment.action.CleanerFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_cleaner
),
PageInfo(
getString(R.string.task_settings),
"com.idormy.sms.forwarder.fragment.action.SettingsFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_settings
),
PageInfo(
getString(R.string.task_frpc),
"com.idormy.sms.forwarder.fragment.action.FrpcFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_frpc
),
PageInfo(
getString(R.string.task_http_server),
"com.idormy.sms.forwarder.fragment.action.HttpServerFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_http_server
),
PageInfo(
getString(R.string.task_rule),
"com.idormy.sms.forwarder.fragment.action.RuleFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_rule
),
PageInfo(
getString(R.string.task_sender),
"com.idormy.sms.forwarder.fragment.action.SenderFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_sender
),
PageInfo(
getString(R.string.task_alarm),
"com.idormy.sms.forwarder.fragment.action.AlarmFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_alarm
),
PageInfo(
getString(R.string.task_resend),
"com.idormy.sms.forwarder.fragment.action.ResendFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_resend
),
PageInfo(
getString(R.string.task_task),
"com.idormy.sms.forwarder.fragment.action.TaskActionFragment",
- "{\"\":\"\"}",
+ "",
CoreAnim.slide,
R.drawable.auto_task_icon_task
),
@@ -556,7 +563,7 @@ class TasksEditFragment : BaseFragment(), View.OnClic
.negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
SettingUtils.enableLocation = true
val serviceIntent = Intent(requireContext(), LocationService::class.java)
- serviceIntent.action = "START"
+ serviceIntent.action = ACTION_START
requireContext().startService(serviceIntent)
}.show()
return
diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/action/SettingsFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/action/SettingsFragment.kt
index 4d9074bf..d39db700 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/fragment/action/SettingsFragment.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/action/SettingsFragment.kt
@@ -164,7 +164,6 @@ class SettingsFragment : BaseFragment(), Vi
binding!!.sbEnableSms.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
- //检查权限是否获取
XXPermissions.with(this)
// 接收 WAP 推送消息
.permission(Permission.RECEIVE_WAP_PUSH)
@@ -175,7 +174,8 @@ class SettingsFragment : BaseFragment(), Vi
// 发送短信
//.permission(Permission.SEND_SMS)
// 读取短信
- .permission(Permission.READ_SMS).request(object : OnPermissionCallback {
+ .permission(Permission.READ_SMS)
+ .request(object : OnPermissionCallback {
override fun onGranted(permissions: List, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
@@ -200,7 +200,6 @@ class SettingsFragment : BaseFragment(), Vi
binding!!.sbEnablePhone.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
- //检查权限是否获取
XXPermissions.with(this)
// 读取电话状态
.permission(Permission.READ_PHONE_STATE)
@@ -209,7 +208,8 @@ class SettingsFragment : BaseFragment(), Vi
// 读取通话记录
.permission(Permission.READ_CALL_LOG)
// 读取联系人
- .permission(Permission.READ_CONTACTS).request(object : OnPermissionCallback {
+ .permission(Permission.READ_CONTACTS)
+ .request(object : OnPermissionCallback {
override fun onGranted(permissions: List, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
@@ -234,37 +234,42 @@ class SettingsFragment : BaseFragment(), Vi
binding!!.sbEnableAppNotify.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
- //检查权限是否获取
- XXPermissions.with(this).permission(Permission.BIND_NOTIFICATION_LISTENER_SERVICE).request(OnPermissionCallback { _, allGranted ->
- if (!allGranted) {
- binding!!.sbEnableAppNotify.isChecked = false
- XToastUtils.error(R.string.tips_notification_listener)
- return@OnPermissionCallback
- }
+ XXPermissions.with(this)
+ .permission(Permission.BIND_NOTIFICATION_LISTENER_SERVICE)
+ .request(OnPermissionCallback { _, allGranted ->
+ if (!allGranted) {
+ binding!!.sbEnableAppNotify.isChecked = false
+ XToastUtils.error(R.string.tips_notification_listener)
+ return@OnPermissionCallback
+ }
- binding!!.sbEnableAppNotify.isChecked = true
- CommonUtils.toggleNotificationListenerService(requireContext())
- })
+ binding!!.sbEnableAppNotify.isChecked = true
+ CommonUtils.toggleNotificationListenerService(requireContext())
+ })
}
}
binding!!.sbEnableLocation.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
- XXPermissions.with(this).permission(Permission.ACCESS_COARSE_LOCATION).permission(Permission.ACCESS_FINE_LOCATION).permission(Permission.ACCESS_BACKGROUND_LOCATION).request(object : OnPermissionCallback {
- override fun onGranted(permissions: List, all: Boolean) {
- }
+ XXPermissions.with(this)
+ .permission(Permission.ACCESS_COARSE_LOCATION)
+ .permission(Permission.ACCESS_FINE_LOCATION)
+ .permission(Permission.ACCESS_BACKGROUND_LOCATION)
+ .request(object : OnPermissionCallback {
+ override fun onGranted(permissions: List, all: Boolean) {
+ }
- override fun onDenied(permissions: List, never: Boolean) {
- if (never) {
- XToastUtils.error(R.string.toast_denied_never)
- // 如果是被永久拒绝就跳转到应用权限系统设置页面
- XXPermissions.startPermissionActivity(requireContext(), permissions)
- } else {
- XToastUtils.error(R.string.toast_denied)
+ override fun onDenied(permissions: List, never: Boolean) {
+ if (never) {
+ XToastUtils.error(R.string.toast_denied_never)
+ // 如果是被永久拒绝就跳转到应用权限系统设置页面
+ XXPermissions.startPermissionActivity(requireContext(), permissions)
+ } else {
+ XToastUtils.error(R.string.toast_denied)
+ }
+ binding!!.sbEnableLocation.isChecked = false
}
- binding!!.sbEnableLocation.isChecked = false
- }
- })
+ })
}
}
//设置位置更新最小时间间隔(单位:毫秒); 默认间隔:10000毫秒,最小间隔:1000毫秒
@@ -296,7 +301,6 @@ class SettingsFragment : BaseFragment(), Vi
binding!!.sbEnableSmsCommand.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
- //检查权限是否获取
XXPermissions.with(this)
// 系统设置
.permission(Permission.WRITE_SETTINGS)
@@ -305,7 +309,8 @@ class SettingsFragment : BaseFragment(), Vi
// 发送短信
.permission(Permission.SEND_SMS)
// 读取短信
- .permission(Permission.READ_SMS).request(object : OnPermissionCallback {
+ .permission(Permission.READ_SMS)
+ .request(object : OnPermissionCallback {
override fun onGranted(permissions: List, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/BluetoothFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/BluetoothFragment.kt
new file mode 100644
index 00000000..4343f897
--- /dev/null
+++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/BluetoothFragment.kt
@@ -0,0 +1,325 @@
+package com.idormy.sms.forwarder.fragment.condition
+
+import android.annotation.SuppressLint
+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.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.gson.Gson
+import com.hjq.permissions.OnPermissionCallback
+import com.hjq.permissions.Permission
+import com.hjq.permissions.XXPermissions
+import com.idormy.sms.forwarder.R
+import com.idormy.sms.forwarder.adapter.BluetoothRecyclerAdapter
+import com.idormy.sms.forwarder.core.BaseFragment
+import com.idormy.sms.forwarder.databinding.FragmentTasksConditionBluetoothBinding
+import com.idormy.sms.forwarder.entity.condition.BluetoothSetting
+import com.idormy.sms.forwarder.service.BluetoothScanService
+import com.idormy.sms.forwarder.utils.ACTION_START
+import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_CONDITION
+import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_CONDITION
+import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_CONDITION
+import com.idormy.sms.forwarder.utils.Log
+import com.idormy.sms.forwarder.utils.SettingUtils
+import com.idormy.sms.forwarder.utils.TASK_CONDITION_BLUETOOTH
+import com.idormy.sms.forwarder.utils.XToastUtils
+import com.xuexiang.xaop.annotation.SingleClick
+import com.xuexiang.xpage.annotation.Page
+import com.xuexiang.xrouter.annotation.AutoWired
+import com.xuexiang.xrouter.launcher.XRouter
+import com.xuexiang.xui.utils.CountDownButtonHelper
+import com.xuexiang.xui.widget.actionbar.TitleBar
+import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
+import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
+
+
+@Page(name = "Bluetooth")
+@Suppress("PrivatePropertyName", "SameParameterValue", "DEPRECATION")
+class BluetoothFragment : BaseFragment(), View.OnClickListener {
+
+ private val TAG: String = BluetoothFragment::class.java.simpleName
+ private var titleBar: TitleBar? = null
+ private var mCountDownHelper: CountDownButtonHelper? = null
+
+ private lateinit var bluetoothAdapter: BluetoothAdapter
+ private lateinit var bluetoothRecyclerAdapter: BluetoothRecyclerAdapter
+ private var discoveredDevices: MutableList = mutableListOf()
+ private val bluetoothReceiver = object : BroadcastReceiver() {
+ @SuppressLint("MissingPermission", "NotifyDataSetChanged")
+ override fun onReceive(context: Context?, intent: Intent?) {
+ val action: String? = intent?.action
+
+ when (action) {
+ BluetoothDevice.ACTION_FOUND -> {
+ val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ device?.let {
+ Log.d(TAG, "Discovered device: ${it.name} - ${it.address}")
+ if (!discoveredDevices.contains(it)) {
+ discoveredDevices.add(it)
+ bluetoothRecyclerAdapter.notifyDataSetChanged()
+ }
+ }
+ }
+
+ BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
+ Log.d(TAG, "Bluetooth scan finished, discoveredDevices: $discoveredDevices")
+ }
+ }
+ }
+ }
+
+ @JvmField
+ @AutoWired(name = KEY_EVENT_DATA_CONDITION)
+ var eventData: String? = null
+
+ override fun initArgs() {
+ XRouter.getInstance().inject(this)
+ }
+
+ override fun viewBindingInflate(
+ inflater: LayoutInflater,
+ container: ViewGroup,
+ ): FragmentTasksConditionBluetoothBinding {
+ return FragmentTasksConditionBluetoothBinding.inflate(inflater, container, false)
+ }
+
+ override fun initTitle(): TitleBar? {
+ titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.task_bluetooth)
+ return titleBar
+ }
+
+ /**
+ * 初始化控件
+ */
+ override fun initViews() {
+ //测试按钮增加倒计时,避免重复点击
+ mCountDownHelper = CountDownButtonHelper(binding!!.btnStartDiscovery, 12)
+ mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
+ override fun onCountDown(time: Int) {
+ binding!!.btnStartDiscovery.text = String.format(getString(R.string.seconds_n), time)
+ }
+
+ override fun onFinished() {
+ requireActivity().unregisterReceiver(bluetoothReceiver)
+ binding!!.btnStartDiscovery.text = getString(R.string.start_discovery)
+ }
+ })
+
+ binding!!.rgBluetoothAction.setOnCheckedChangeListener { _, checkedId ->
+ Log.d(TAG, "rgBluetoothState checkedId:$checkedId")
+ when (checkedId) {
+ R.id.rb_action_state_changed -> {
+ binding!!.layoutBluetoothState.visibility = View.VISIBLE
+ binding!!.layoutDiscoveryFinished.visibility = View.GONE
+ binding!!.layoutDeviceAddress.visibility = View.GONE
+ }
+
+ R.id.rb_action_discovery_finished -> {
+ binding!!.layoutBluetoothState.visibility = View.GONE
+ binding!!.layoutDiscoveryFinished.visibility = View.VISIBLE
+ binding!!.layoutDeviceAddress.visibility = View.VISIBLE
+ }
+
+ else -> {
+ binding!!.layoutBluetoothState.visibility = View.GONE
+ binding!!.layoutDiscoveryFinished.visibility = View.GONE
+ binding!!.layoutDeviceAddress.visibility = View.VISIBLE
+ }
+ }
+ checkSetting(true)
+ }
+
+ Log.d(TAG, "initViews eventData:$eventData")
+ if (eventData != null) {
+ val settingVo = Gson().fromJson(eventData, BluetoothSetting::class.java)
+ Log.d(TAG, "initViews settingVo:$settingVo")
+ binding!!.tvDescription.text = settingVo.description
+ binding!!.rgBluetoothAction.check(settingVo.getActionCheckId())
+ binding!!.rgBluetoothState.check(settingVo.getStateCheckId())
+ binding!!.rgDiscoveryResult.check(settingVo.getResultCheckId())
+ binding!!.etDeviceAddress.setText(settingVo.device)
+ } else {
+ binding!!.rgBluetoothAction.check(R.id.rb_action_state_changed)
+ binding!!.rgBluetoothState.check(R.id.rb_state_on)
+ binding!!.rgDiscoveryResult.check(R.id.rb_discovered)
+ }
+ }
+
+ @SuppressLint("SetTextI18n")
+ override fun initListeners() {
+ binding!!.btnStartDiscovery.setOnClickListener(this)
+ binding!!.btnDel.setOnClickListener(this)
+ binding!!.btnSave.setOnClickListener(this)
+ binding!!.rgBluetoothState.setOnCheckedChangeListener { _, _ ->
+ checkSetting(true)
+ }
+ binding!!.rgDiscoveryResult.setOnCheckedChangeListener { _, _ ->
+ checkSetting(true)
+ }
+ binding!!.etDeviceAddress.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
+ override fun afterTextChanged(s: Editable?) {
+ checkSetting(true)
+ }
+ })
+
+ binding!!.recyclerDevices.layoutManager = LinearLayoutManager(requireContext())
+ bluetoothRecyclerAdapter = BluetoothRecyclerAdapter(discoveredDevices, { position ->
+ val device = discoveredDevices[position]
+ binding!!.etDeviceAddress.setText(device.address)
+ })
+ binding!!.recyclerDevices.adapter = bluetoothRecyclerAdapter
+
+ bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
+ @Suppress("SENSELESS_COMPARISON")
+ if (bluetoothAdapter == null) {
+ XToastUtils.error(getString(R.string.bluetooth_not_supported))
+ return
+ }
+
+ // 启动蓝牙搜索
+ // startBluetoothDiscovery()
+ }
+
+ override fun onDestroyView() {
+ if (mCountDownHelper != null) mCountDownHelper!!.recycle()
+ if (bluetoothReceiver.isOrderedBroadcast) {
+ requireActivity().unregisterReceiver(bluetoothReceiver)
+ }
+ super.onDestroyView()
+ }
+
+ @SingleClick
+ override fun onClick(v: View) {
+ try {
+ when (v.id) {
+
+ R.id.btn_start_discovery -> {
+ if (!SettingUtils.enableBluetooth) {
+ MaterialDialog.Builder(requireContext())
+ .iconRes(R.drawable.auto_task_icon_location)
+ .title(R.string.enable_bluetooth)
+ .content(R.string.enable_bluetooth_dialog)
+ .cancelable(false)
+ .positiveText(R.string.lab_yes)
+ .negativeText(R.string.lab_no)
+ .onPositive { _: MaterialDialog?, _: DialogAction? ->
+ XXPermissions.with(this)
+ .permission(Permission.BLUETOOTH_SCAN)
+ .permission(Permission.BLUETOOTH_CONNECT)
+ .permission(Permission.BLUETOOTH_ADVERTISE)
+ .permission(Permission.ACCESS_FINE_LOCATION)
+ .request(object : OnPermissionCallback {
+ override fun onGranted(permissions: List, all: Boolean) {
+ startBluetoothDiscovery()
+ Log.d(TAG, "onGranted: permissions=$permissions, all=$all")
+ if (!all) {
+ XToastUtils.warning(getString(R.string.toast_granted_part))
+ }
+ SettingUtils.enableBluetooth = true
+ val serviceIntent = Intent(requireContext(), BluetoothScanService::class.java)
+ serviceIntent.action = ACTION_START
+ requireContext().startService(serviceIntent)
+ }
+
+ override fun onDenied(permissions: List, never: Boolean) {
+ Log.e(TAG, "onDenied: permissions=$permissions, never=$never")
+ if (never) {
+ XToastUtils.error(getString(R.string.toast_denied_never))
+ XXPermissions.startPermissionActivity(requireContext(), permissions)
+ } else {
+ XToastUtils.error(getString(R.string.toast_denied))
+ }
+ }
+ })
+ }.show()
+ return
+ }
+
+ startBluetoothDiscovery()
+ return
+ }
+
+ R.id.btn_del -> {
+ popToBack()
+ return
+ }
+
+ R.id.btn_save -> {
+ val settingVo = checkSetting()
+ val intent = Intent()
+ intent.putExtra(KEY_BACK_DESCRIPTION_CONDITION, settingVo.description)
+ intent.putExtra(KEY_BACK_DATA_CONDITION, Gson().toJson(settingVo))
+ setFragmentResult(TASK_CONDITION_BLUETOOTH, intent)
+ popToBack()
+ return
+ }
+ }
+ } catch (e: Exception) {
+ XToastUtils.error(e.message.toString(), 30000)
+ e.printStackTrace()
+ Log.e(TAG, "onClick error:$e")
+ }
+ }
+
+ @SuppressLint("MissingPermission", "NotifyDataSetChanged")
+ private fun startBluetoothDiscovery() {
+ try {
+ mCountDownHelper?.start()
+
+ if (bluetoothAdapter.isDiscovering) {
+ bluetoothAdapter.cancelDiscovery()
+ }
+
+ // 注册广播接收器
+ val filter = IntentFilter().apply {
+ addAction(BluetoothDevice.ACTION_FOUND)
+ addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
+ }
+ requireActivity().registerReceiver(bluetoothReceiver, filter)
+
+ discoveredDevices.clear()
+ bluetoothRecyclerAdapter.notifyDataSetChanged()
+ bluetoothAdapter.startDiscovery()
+ } catch (e: Exception) {
+ mCountDownHelper?.finish()
+ XToastUtils.error(e.message.toString(), 30000)
+ Log.e(TAG, "startBluetoothDiscovery error:$e")
+ }
+ }
+
+ //检查设置
+ private fun checkSetting(updateView: Boolean = false): BluetoothSetting {
+ val actionCheckId = binding!!.rgBluetoothAction.checkedRadioButtonId
+ val deviceAddress = binding!!.etDeviceAddress.text.toString().trim()
+ if (actionCheckId != R.id.rb_action_state_changed &&
+ (deviceAddress.isEmpty() || !BluetoothAdapter.checkBluetoothAddress(deviceAddress))
+ ) {
+ if (updateView) {
+ binding!!.etDeviceAddress.error = getString(R.string.mac_error)
+ } else {
+ throw Exception(getString(R.string.invalid_bluetooth_mac_address))
+ }
+ } else {
+ binding!!.etDeviceAddress.error = null
+ }
+
+ val stateCheckId = binding!!.rgBluetoothState.checkedRadioButtonId
+ val resultCheckId = binding!!.rgDiscoveryResult.checkedRadioButtonId
+ val settingVo = BluetoothSetting(actionCheckId, stateCheckId, resultCheckId, deviceAddress)
+ if (updateView) {
+ binding!!.tvDescription.text = settingVo.description
+ }
+
+ return settingVo
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/LeaveAddressFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/LeaveAddressFragment.kt
index 06e89692..2efca2b7 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/LeaveAddressFragment.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/LeaveAddressFragment.kt
@@ -14,6 +14,7 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentTasksConditionLeaveAddressBinding
import com.idormy.sms.forwarder.entity.condition.LocationSetting
import com.idormy.sms.forwarder.service.LocationService
+import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_CONDITION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_CONDITION
@@ -172,12 +173,19 @@ class LeaveAddressFragment : BaseFragment {
if (!App.LocationClient.isStarted()) {
- MaterialDialog.Builder(requireContext()).iconRes(R.drawable.auto_task_icon_location).title(R.string.enable_location).content(R.string.enable_location_dialog).cancelable(false).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
- SettingUtils.enableLocation = true
- val serviceIntent = Intent(requireContext(), LocationService::class.java)
- serviceIntent.action = "START"
- requireContext().startService(serviceIntent)
- }.show()
+ MaterialDialog.Builder(requireContext())
+ .iconRes(R.drawable.auto_task_icon_location)
+ .title(R.string.enable_location)
+ .content(R.string.enable_location_dialog)
+ .cancelable(false)
+ .positiveText(R.string.lab_yes)
+ .negativeText(R.string.lab_no)
+ .onPositive { _: MaterialDialog?, _: DialogAction? ->
+ SettingUtils.enableLocation = true
+ val serviceIntent = Intent(requireContext(), LocationService::class.java)
+ serviceIntent.action = ACTION_START
+ requireContext().startService(serviceIntent)
+ }.show()
return
}
diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/ToAddressFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/ToAddressFragment.kt
index 19a6c8e1..f7ab1787 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/ToAddressFragment.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/ToAddressFragment.kt
@@ -14,6 +14,7 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentTasksConditionToAddressBinding
import com.idormy.sms.forwarder.entity.condition.LocationSetting
import com.idormy.sms.forwarder.service.LocationService
+import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_CONDITION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_CONDITION
@@ -175,7 +176,7 @@ class ToAddressFragment : BaseFragment(
MaterialDialog.Builder(requireContext()).iconRes(R.drawable.auto_task_icon_location).title(R.string.enable_location).content(R.string.enable_location_dialog).cancelable(false).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
SettingUtils.enableLocation = true
val serviceIntent = Intent(requireContext(), LocationService::class.java)
- serviceIntent.action = "START"
+ serviceIntent.action = ACTION_START
requireContext().startService(serviceIntent)
}.show()
return
diff --git a/app/src/main/java/com/idormy/sms/forwarder/receiver/BluetoothReceiver.kt b/app/src/main/java/com/idormy/sms/forwarder/receiver/BluetoothReceiver.kt
new file mode 100644
index 00000000..59523f19
--- /dev/null
+++ b/app/src/main/java/com/idormy/sms/forwarder/receiver/BluetoothReceiver.kt
@@ -0,0 +1,192 @@
+package com.idormy.sms.forwarder.receiver
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Handler
+import androidx.core.app.ActivityCompat
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.workDataOf
+import com.google.gson.Gson
+import com.idormy.sms.forwarder.App
+import com.idormy.sms.forwarder.service.BluetoothScanService
+import com.idormy.sms.forwarder.utils.ACTION_RESTART
+import com.idormy.sms.forwarder.utils.ACTION_START
+import com.idormy.sms.forwarder.utils.ACTION_STOP
+import com.idormy.sms.forwarder.utils.Log
+import com.idormy.sms.forwarder.utils.SettingUtils
+import com.idormy.sms.forwarder.utils.TASK_CONDITION_BLUETOOTH
+import com.idormy.sms.forwarder.utils.TaskWorker
+import com.idormy.sms.forwarder.utils.task.TaskUtils
+import com.idormy.sms.forwarder.workers.BluetoothWorker
+
+@Suppress("PrivatePropertyName", "DEPRECATION")
+@SuppressLint("MissingPermission")
+class BluetoothReceiver : BroadcastReceiver() {
+
+ private val TAG: String = BluetoothReceiver::class.java.simpleName
+ private val handler = Handler()
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (context == null || intent == null) return
+
+ when (val action = intent.action) {
+ // 发现设备
+ BluetoothDevice.ACTION_FOUND -> {
+ val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ device?.let {
+ if (ActivityCompat.checkSelfPermission(App.context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) return
+ if (SettingUtils.bluetoothIgnoreAnonymous && it.name.isNullOrEmpty()) return
+
+ //TODO: 实测这里一台设备会收到两次广播
+ Log.d(TAG, "Discovered device: ${it.name} - ${it.address}")
+ val discoveredDevices = TaskUtils.discoveredDevices
+ discoveredDevices[it.address] = it.name ?: ""
+ TaskUtils.discoveredDevices = discoveredDevices
+ }
+ }
+
+ // 扫描完成
+ BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
+ //TODO: 放在这里去判断是否已经发现某个设备(避免 ACTION_FOUND 重复广播)
+ Log.d(TAG, "Bluetooth scan finished, discoveredDevices: ${TaskUtils.discoveredDevices}")
+ if (TaskUtils.discoveredDevices.isNotEmpty()) {
+ handleWorkRequest(context, action, Gson().toJson(TaskUtils.discoveredDevices))
+ }
+
+ restartBluetoothService(ACTION_STOP)
+ if (SettingUtils.enableBluetooth) {
+ Log.d(TAG, "Bluetooth scan finished, restart in ${SettingUtils.bluetoothScanInterval}ms")
+ handler.postDelayed({
+ restartBluetoothService(ACTION_START)
+ }, SettingUtils.bluetoothScanInterval)
+ }
+ }
+
+ // 蓝牙状态变化
+ BluetoothAdapter.ACTION_STATE_CHANGED -> {
+ val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
+ handleBluetoothStateChanged(state)
+ handleWorkRequest(context, action, state.toString())
+ }
+
+ // 蓝牙扫描模式变化
+ BluetoothAdapter.ACTION_SCAN_MODE_CHANGED -> {
+ if (SettingUtils.enableBluetooth) {
+ restartBluetoothService()
+ }
+ }
+
+ // 本地蓝牙名称变化
+ BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED -> {
+ }
+
+ // 蓝牙连接状态变化
+ BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED -> {
+ }
+
+ // 蓝牙设备的配对状态变化
+ BluetoothDevice.ACTION_BOND_STATE_CHANGED -> {
+ }
+
+ // 蓝牙设备连接
+ BluetoothDevice.ACTION_ACL_CONNECTED -> {
+ val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ if (device != null) {
+ Log.d(TAG, "Connected device: ${device.name} - ${device.address}")
+ TaskUtils.connectedDevices[device.address] = device.name
+ handleWorkRequest(context, action, Gson().toJson(mutableMapOf(device.address to device.name)))
+ }
+ }
+
+ // 蓝牙设备断开连接
+ BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
+ val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
+ if (device != null) {
+ Log.d(TAG, "Disconnected device: ${device.name} - ${device.address}")
+ TaskUtils.connectedDevices.remove(device.address)
+ handleWorkRequest(context, action, Gson().toJson(mutableMapOf(device.address to device.name)))
+ }
+ }
+ }
+ }
+
+ // 处理蓝牙状态变化
+ private fun handleBluetoothStateChanged(state: Int) {
+ when (state) {
+ // 蓝牙已关闭
+ BluetoothAdapter.STATE_OFF -> {
+ Log.d(TAG, "BluetoothAdapter.STATE_OFF")
+ TaskUtils.bluetoothState = state
+ // 停止扫描 & 删除任何挂起的延迟扫描任务
+ restartBluetoothService(ACTION_STOP)
+ handler.removeCallbacksAndMessages(null)
+ }
+
+ // 蓝牙已打开
+ BluetoothAdapter.STATE_ON -> {
+ Log.d(TAG, "BluetoothAdapter.STATE_ON")
+ TaskUtils.bluetoothState = state
+ if (SettingUtils.enableBluetooth) {
+ restartBluetoothService(ACTION_START)
+ }
+ }
+
+ // 蓝牙正在打开
+ BluetoothAdapter.STATE_TURNING_ON -> {
+ Log.d(TAG, "BluetoothAdapter.STATE_TURNING_ON")
+ }
+
+ // 蓝牙正在关闭
+ BluetoothAdapter.STATE_TURNING_OFF -> {
+ Log.d(TAG, "BluetoothAdapter.STATE_TURNING_OFF")
+ }
+
+ // 蓝牙正在连接
+ BluetoothAdapter.STATE_CONNECTING -> {
+ Log.d(TAG, "BluetoothAdapter.STATE_CONNECTING")
+ }
+
+ // 蓝牙已连接
+ BluetoothAdapter.STATE_CONNECTED -> {
+ Log.d(TAG, "BluetoothAdapter.STATE_CONNECTED")
+ }
+
+ // 蓝牙正在断开连接
+ BluetoothAdapter.STATE_DISCONNECTING -> {
+ Log.d(TAG, "BluetoothAdapter.STATE_DISCONNECTING")
+ }
+
+ // 蓝牙已断开连接
+ BluetoothAdapter.STATE_DISCONNECTED -> {
+ Log.d(TAG, "BluetoothAdapter.STATE_DISCONNECTED")
+ }
+ }
+ }
+
+ //重启蓝牙扫描服务
+ private fun restartBluetoothService(action: String = ACTION_RESTART) {
+ Log.d(TAG, "restartBluetoothService, action: $action")
+ val serviceIntent = Intent(App.context, BluetoothScanService::class.java)
+ serviceIntent.action = action
+ App.context.startService(serviceIntent)
+ }
+
+ private fun handleWorkRequest(context: Context, action: String, msg: String) {
+ val request = OneTimeWorkRequestBuilder()
+ .setInputData(
+ workDataOf(
+ TaskWorker.CONDITION_TYPE to TASK_CONDITION_BLUETOOTH,
+ TaskWorker.ACTION to action,
+ TaskWorker.MSG to msg,
+ )
+ ).build()
+ WorkManager.getInstance(context).enqueue(request)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/idormy/sms/forwarder/service/BluetoothScanService.kt b/app/src/main/java/com/idormy/sms/forwarder/service/BluetoothScanService.kt
new file mode 100644
index 00000000..b21eff0f
--- /dev/null
+++ b/app/src/main/java/com/idormy/sms/forwarder/service/BluetoothScanService.kt
@@ -0,0 +1,70 @@
+package com.idormy.sms.forwarder.service
+
+import android.Manifest
+import android.app.Service
+import android.bluetooth.BluetoothAdapter
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.IBinder
+import androidx.core.app.ActivityCompat
+import com.idormy.sms.forwarder.utils.ACTION_RESTART
+import com.idormy.sms.forwarder.utils.ACTION_START
+import com.idormy.sms.forwarder.utils.ACTION_STOP
+import com.idormy.sms.forwarder.utils.Log
+
+@Suppress("PrivatePropertyName", "DEPRECATION")
+class BluetoothScanService : Service() {
+
+ private val TAG: String = BluetoothScanService::class.java.simpleName
+ private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter()
+
+ companion object {
+ var isRunning = false
+ }
+
+ override fun onBind(p0: Intent?): IBinder? {
+ return null
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null) return START_NOT_STICKY
+ Log.i(TAG, "onStartCommand: ${intent.action}")
+
+ when (intent.action) {
+ ACTION_START -> startDiscovery()
+ ACTION_STOP -> stopDiscovery()
+ ACTION_RESTART -> {
+ stopDiscovery()
+ startDiscovery()
+ }
+ }
+ return START_NOT_STICKY
+ }
+
+ // 开始扫描蓝牙设备
+ private fun startDiscovery() {
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+ if (isRunning) return
+ bluetoothAdapter?.startDiscovery()
+ isRunning = true
+ }
+
+ // 停止蓝牙扫描
+ private fun stopDiscovery() {
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
+ return
+ }
+ if (!isRunning) return
+ bluetoothAdapter?.cancelDiscovery()
+ isRunning = false
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ isRunning = false
+ stopDiscovery()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/idormy/sms/forwarder/service/ForegroundService.kt b/app/src/main/java/com/idormy/sms/forwarder/service/ForegroundService.kt
index 1702ceba..8a002cbf 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/service/ForegroundService.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/service/ForegroundService.kt
@@ -1,7 +1,11 @@
package com.idormy.sms.forwarder.service
import android.annotation.SuppressLint
-import android.app.*
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
@@ -20,7 +24,22 @@ import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.activity.MainActivity
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.entity.action.AlarmSetting
-import com.idormy.sms.forwarder.utils.*
+import com.idormy.sms.forwarder.utils.ACTION_START
+import com.idormy.sms.forwarder.utils.ACTION_STOP
+import com.idormy.sms.forwarder.utils.ACTION_STOP_ALARM
+import com.idormy.sms.forwarder.utils.ACTION_UPDATE_NOTIFICATION
+import com.idormy.sms.forwarder.utils.CommonUtils
+import com.idormy.sms.forwarder.utils.EVENT_ALARM_ACTION
+import com.idormy.sms.forwarder.utils.EVENT_FRPC_RUNNING_ERROR
+import com.idormy.sms.forwarder.utils.EVENT_FRPC_RUNNING_SUCCESS
+import com.idormy.sms.forwarder.utils.EXTRA_UPDATE_NOTIFICATION
+import com.idormy.sms.forwarder.utils.FRONT_CHANNEL_ID
+import com.idormy.sms.forwarder.utils.FRONT_CHANNEL_NAME
+import com.idormy.sms.forwarder.utils.FRONT_NOTIFY_ID
+import com.idormy.sms.forwarder.utils.INTENT_FRPC_APPLY_FILE
+import com.idormy.sms.forwarder.utils.Log
+import com.idormy.sms.forwarder.utils.SettingUtils
+import com.idormy.sms.forwarder.utils.TASK_CONDITION_CRON
import com.idormy.sms.forwarder.utils.task.CronJobScheduler
import com.idormy.sms.forwarder.workers.LoadAppListWorker
import com.jeremyliao.liveeventbus.LiveEventBus
@@ -167,20 +186,20 @@ class ForegroundService : Service() {
if (intent != null) {
when (intent.action) {
- "START" -> {
+ ACTION_START -> {
startForegroundService()
}
- "STOP" -> {
+ ACTION_STOP -> {
stopForegroundService()
}
- "UPDATE_NOTIFICATION" -> {
- val updatedContent = intent.getStringExtra("UPDATED_CONTENT")
+ ACTION_UPDATE_NOTIFICATION -> {
+ val updatedContent = intent.getStringExtra(EXTRA_UPDATE_NOTIFICATION)
updateNotification(updatedContent ?: "")
}
- "STOP_ALARM" -> {
+ ACTION_STOP_ALARM -> {
alarmPlayer?.release()
alarmPlayer = null
updateNotification(SettingUtils.notifyContent)
@@ -307,7 +326,7 @@ class ForegroundService : Service() {
// 添加停止按钮(可选)
if (showStopButton) {
val stopIntent = Intent(this, ForegroundService::class.java).apply {
- action = "STOP_ALARM"
+ action = ACTION_STOP_ALARM
}
val stopPendingIntent = PendingIntent.getService(this, 0, stopIntent, flags)
builder.addAction(R.drawable.ic_stop, getString(R.string.stop), stopPendingIntent)
diff --git a/app/src/main/java/com/idormy/sms/forwarder/service/LocationService.kt b/app/src/main/java/com/idormy/sms/forwarder/service/LocationService.kt
index 88227696..b880c3b5 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/service/LocationService.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/service/LocationService.kt
@@ -15,6 +15,9 @@ import androidx.work.workDataOf
import com.google.gson.Gson
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.entity.LocationInfo
+import com.idormy.sms.forwarder.utils.ACTION_RESTART
+import com.idormy.sms.forwarder.utils.ACTION_START
+import com.idormy.sms.forwarder.utils.ACTION_STOP
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.LocationUtils
import com.idormy.sms.forwarder.utils.Log
@@ -64,19 +67,14 @@ class LocationService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
-
if (intent == null) return START_NOT_STICKY
-
Log.i(TAG, "onStartCommand: ${intent.action}")
- if (intent.action == "START" && !isRunning) {
- startService()
- } else if (intent.action == "STOP" && isRunning) {
- stopService()
- } else if (intent.action == "RESTART") {
- restartLocation()
+ when {
+ intent.action == ACTION_START && !isRunning -> startService()
+ intent.action == ACTION_STOP && isRunning -> stopService()
+ intent.action == ACTION_RESTART -> restartLocation()
}
-
return START_STICKY
}
diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/BluetoothUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/BluetoothUtils.kt
new file mode 100644
index 00000000..f67b491c
--- /dev/null
+++ b/app/src/main/java/com/idormy/sms/forwarder/utils/BluetoothUtils.kt
@@ -0,0 +1,38 @@
+package com.idormy.sms.forwarder.utils
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.core.content.ContextCompat
+
+@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate")
+object BluetoothUtils {
+
+ /**
+ * 检查应用是否具有蓝牙权限
+ */
+ fun hasBluetoothPermission(context: Context): Boolean {
+ return ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED
+ && ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
+ }
+
+ /**
+ * 检查蓝牙是否已启用
+ */
+ fun isBluetoothEnabled(): Boolean {
+ val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
+ return bluetoothAdapter != null && bluetoothAdapter.isEnabled
+ }
+
+ /**
+ * 检查设备是否支持蓝牙功能
+ */
+ fun hasBluetoothCapability(context: Context): Boolean {
+ if (!hasBluetoothPermission(context)) {
+ Log.e("BluetoothUtils", "hasBluetoothCapability: no bluetooth permission")
+ return false
+ }
+
+ return context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)
+ }
+}
diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/Constants.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/Constants.kt
index ddf779a4..675abee5 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/utils/Constants.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/utils/Constants.kt
@@ -19,6 +19,14 @@ object TaskWorker {
const val ACTION = "action"
}
+//服务相关
+const val ACTION_START = "START"
+const val ACTION_STOP = "STOP"
+const val ACTION_RESTART = "RESTART"
+const val ACTION_STOP_ALARM = "STOP_ALARM"
+const val ACTION_UPDATE_NOTIFICATION = "UPDATE_NOTIFICATION"
+const val EXTRA_UPDATE_NOTIFICATION = "EXTRA_UPDATE_NOTIFICATION"
+
//初始化相关
const val AUTO_CHECK_UPDATE = "auto_check_update"
const val JOIN_PREVIEW_PROGRAM = "join_preview_program"
@@ -84,6 +92,10 @@ const val SP_LOCATION_POWER_REQUIREMENT = "location_power_requirement"
const val SP_LOCATION_MIN_INTERVAL = "location_min_interval_time"
const val SP_LOCATION_MIN_DISTANCE = "location_min_distance"
+const val SP_BLUETOOTH = "enable_bluetooth"
+const val SP_BLUETOOTH_SCAN_INTERVAL = "bluetooth_scan_interval"
+const val SP_BLUETOOTH_IGNORE_ANONYMOUS = "bluetooth_ignore_anonymous"
+
const val SP_ENABLE_CACTUS = "enable_cactus"
const val CACTUS_TIMER = "cactus_timer"
const val CACTUS_LAST_TIMER = "cactus_last_timer"
@@ -239,6 +251,7 @@ const val TASK_CONDITION_LOCK_SCREEN = 1007
const val TASK_CONDITION_SMS = 1008
const val TASK_CONDITION_CALL = 1009
const val TASK_CONDITION_APP = 1010
+const val TASK_CONDITION_BLUETOOTH = 1011
//注意:TASK_ACTION_XXX 枚举值 等于 TASK_ACTION_FRAGMENT_LIST 索引加上 KEY_BACK_CODE_ACTION,不可改变
const val TASK_ACTION_SENDSMS = 2000
@@ -268,6 +281,9 @@ const val SP_SIM_STATE = "sim_state"
const val SP_LOCATION_INFO_OLD = "location_info_old"
const val SP_LOCATION_INFO_NEW = "location_info_new"
const val SP_LOCK_SCREEN_ACTION = "lock_screen_action"
+const val SP_CONNECTED_DEVICE = "connected_device"
+const val SP_DISCOVERED_DEVICES = "discovered_devices"
+const val SP_BLUETOOTH_STATE = "bluetooth_state"
//SIM卡已准备就绪时,延迟5秒(给够搜索信号时间)才执行任务
const val DELAY_TIME_AFTER_SIM_READY = 5000L
diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtils.kt
index 3e21bc48..88ed8636 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtils.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/utils/SettingUtils.kt
@@ -148,6 +148,15 @@ class SettingUtils private constructor() {
//是否跟随系统语言
//var isFlowSystemLanguage: Boolean by SharedPreference(SP_IS_FLOW_SYSTEM_LANGUAGE, false)
+
+ //是否启用发现蓝牙设备服务
+ var enableBluetooth: Boolean by SharedPreference(SP_BLUETOOTH, false)
+
+ //扫描蓝牙设备间隔
+ var bluetoothScanInterval: Long by SharedPreference(SP_BLUETOOTH_SCAN_INTERVAL, 10000L)
+
+ //是否忽略匿名设备
+ var bluetoothIgnoreAnonymous: Boolean by SharedPreference(SP_BLUETOOTH_IGNORE_ANONYMOUS, true)
}
init {
diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/task/ConditionUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/task/ConditionUtils.kt
index e4b9d368..18370fed 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/utils/task/ConditionUtils.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/utils/task/ConditionUtils.kt
@@ -1,10 +1,13 @@
package com.idormy.sms.forwarder.utils.task
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
import android.os.BatteryManager
import com.google.gson.Gson
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.entity.TaskSetting
import com.idormy.sms.forwarder.entity.condition.BatterySetting
+import com.idormy.sms.forwarder.entity.condition.BluetoothSetting
import com.idormy.sms.forwarder.entity.condition.ChargeSetting
import com.idormy.sms.forwarder.entity.condition.CronSetting
import com.idormy.sms.forwarder.entity.condition.LocationSetting
@@ -15,6 +18,7 @@ import com.idormy.sms.forwarder.utils.DELAY_TIME_AFTER_SIM_READY
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.TASK_CONDITION_APP
import com.idormy.sms.forwarder.utils.TASK_CONDITION_BATTERY
+import com.idormy.sms.forwarder.utils.TASK_CONDITION_BLUETOOTH
import com.idormy.sms.forwarder.utils.TASK_CONDITION_CALL
import com.idormy.sms.forwarder.utils.TASK_CONDITION_CHARGE
import com.idormy.sms.forwarder.utils.TASK_CONDITION_CRON
@@ -228,6 +232,49 @@ class ConditionUtils private constructor() {
//TODO: 判断消息是否满足条件
}
+ TASK_CONDITION_BLUETOOTH -> {
+ val bluetoothSetting = Gson().fromJson(condition.setting, BluetoothSetting::class.java)
+ if (bluetoothSetting == null) {
+ Log.d(TAG, "TASK-$taskId:bluetoothSetting is null")
+ continue
+ }
+
+ when (bluetoothSetting.action) {
+ BluetoothAdapter.ACTION_STATE_CHANGED -> {
+ if (TaskUtils.bluetoothState != bluetoothSetting.state) {
+ Log.d(TAG, "TASK-$taskId:bluetoothState is not match, bluetoothSetting = $bluetoothSetting")
+ return false
+ }
+ }
+
+ BluetoothDevice.ACTION_ACL_CONNECTED -> {
+ if (!TaskUtils.connectedDevices.containsKey(bluetoothSetting.device)) {
+ Log.d(TAG, "TASK-$taskId:device is not connected, bluetoothSetting = $bluetoothSetting")
+ return false
+ }
+ }
+
+ BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
+ if (TaskUtils.connectedDevices.containsKey(bluetoothSetting.device)) {
+ Log.d(TAG, "TASK-$taskId:device is connected, bluetoothSetting = $bluetoothSetting")
+ return false
+ }
+ }
+
+ BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
+ if (bluetoothSetting.result == 1 && !TaskUtils.discoveredDevices.containsKey(bluetoothSetting.device)) {
+ Log.d(TAG, "TASK-$taskId:device is not discovered, bluetoothSetting = $bluetoothSetting")
+ return false
+ } else if (bluetoothSetting.result == 0 && TaskUtils.discoveredDevices.containsKey(bluetoothSetting.device)) {
+ Log.d(TAG, "TASK-$taskId:device is discovered, bluetoothSetting = $bluetoothSetting")
+ return false
+ }
+ }
+ }
+
+ Log.d(TAG, "TASK-$taskId:bluetoothAction is match, bluetoothSetting = $bluetoothSetting")
+ }
+
}
}
diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/task/TaskUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/task/TaskUtils.kt
index acc40802..45a88b28 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/utils/task/TaskUtils.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/utils/task/TaskUtils.kt
@@ -1,5 +1,6 @@
package com.idormy.sms.forwarder.utils.task
+import android.bluetooth.BluetoothAdapter
import android.os.BatteryManager
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.entity.LocationInfo
@@ -8,7 +9,10 @@ import com.idormy.sms.forwarder.utils.SP_BATTERY_LEVEL
import com.idormy.sms.forwarder.utils.SP_BATTERY_PCT
import com.idormy.sms.forwarder.utils.SP_BATTERY_PLUGGED
import com.idormy.sms.forwarder.utils.SP_BATTERY_STATUS
+import com.idormy.sms.forwarder.utils.SP_BLUETOOTH_STATE
+import com.idormy.sms.forwarder.utils.SP_CONNECTED_DEVICE
import com.idormy.sms.forwarder.utils.SP_DATA_SIM_SLOT
+import com.idormy.sms.forwarder.utils.SP_DISCOVERED_DEVICES
import com.idormy.sms.forwarder.utils.SP_IPV4
import com.idormy.sms.forwarder.utils.SP_IPV6
import com.idormy.sms.forwarder.utils.SP_LOCATION_INFO_NEW
@@ -30,6 +34,7 @@ import com.idormy.sms.forwarder.utils.TASK_ACTION_SENDSMS
import com.idormy.sms.forwarder.utils.TASK_ACTION_SETTINGS
import com.idormy.sms.forwarder.utils.TASK_CONDITION_APP
import com.idormy.sms.forwarder.utils.TASK_CONDITION_BATTERY
+import com.idormy.sms.forwarder.utils.TASK_CONDITION_BLUETOOTH
import com.idormy.sms.forwarder.utils.TASK_CONDITION_CALL
import com.idormy.sms.forwarder.utils.TASK_CONDITION_CHARGE
import com.idormy.sms.forwarder.utils.TASK_CONDITION_CRON
@@ -61,6 +66,7 @@ class TaskUtils private constructor() {
TASK_CONDITION_SMS -> R.drawable.auto_task_icon_sms
TASK_CONDITION_CALL -> R.drawable.auto_task_icon_incall
TASK_CONDITION_APP -> R.drawable.auto_task_icon_start_activity
+ TASK_CONDITION_BLUETOOTH -> R.drawable.auto_task_icon_bluetooth
TASK_ACTION_SENDSMS -> R.drawable.auto_task_icon_sms
TASK_ACTION_NOTIFICATION -> R.drawable.auto_task_icon_notification
TASK_ACTION_CLEANER -> R.drawable.auto_task_icon_cleaner
@@ -89,6 +95,7 @@ class TaskUtils private constructor() {
TASK_CONDITION_SMS -> R.drawable.auto_task_icon_sms_grey
TASK_CONDITION_CALL -> R.drawable.auto_task_icon_incall_grey
TASK_CONDITION_APP -> R.drawable.auto_task_icon_start_activity_grey
+ TASK_CONDITION_BLUETOOTH -> R.drawable.auto_task_icon_bluetooth_grey
TASK_ACTION_SENDSMS -> R.drawable.auto_task_icon_sms_grey
TASK_ACTION_NOTIFICATION -> R.drawable.auto_task_icon_notification_grey
TASK_ACTION_CLEANER -> R.drawable.auto_task_icon_cleaner_grey
@@ -145,5 +152,14 @@ class TaskUtils private constructor() {
//上次锁屏广播
var lockScreenAction: String by SharedPreference(SP_LOCK_SCREEN_ACTION, "")
+ //已发现的蓝牙设备
+ var discoveredDevices: MutableMap by SharedPreference(SP_DISCOVERED_DEVICES, mutableMapOf())
+
+ //已连接的蓝牙设备
+ var connectedDevices: MutableMap by SharedPreference(SP_CONNECTED_DEVICE, mutableMapOf())
+
+ //蓝牙状态
+ var bluetoothState: Int by SharedPreference(SP_BLUETOOTH_STATE, BluetoothAdapter.STATE_ON)
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/idormy/sms/forwarder/workers/ActionWorker.kt b/app/src/main/java/com/idormy/sms/forwarder/workers/ActionWorker.kt
index 0912b4cd..57b38a60 100644
--- a/app/src/main/java/com/idormy/sms/forwarder/workers/ActionWorker.kt
+++ b/app/src/main/java/com/idormy/sms/forwarder/workers/ActionWorker.kt
@@ -28,6 +28,7 @@ import com.idormy.sms.forwarder.entity.action.SmsSetting
import com.idormy.sms.forwarder.entity.action.TaskActionSetting
import com.idormy.sms.forwarder.service.HttpServerService
import com.idormy.sms.forwarder.service.LocationService
+import com.idormy.sms.forwarder.utils.ACTION_RESTART
import com.idormy.sms.forwarder.utils.CacheUtils
import com.idormy.sms.forwarder.utils.EVENT_ALARM_ACTION
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
@@ -203,7 +204,7 @@ class ActionWorker(context: Context, params: WorkerParameters) : CoroutineWorker
if (settingsSetting.enableLocation) {
val serviceIntent = Intent(App.context, LocationService::class.java)
- serviceIntent.action = "RESTART"
+ serviceIntent.action = ACTION_RESTART
App.context.startService(serviceIntent)
}
diff --git a/app/src/main/java/com/idormy/sms/forwarder/workers/BluetoothWorker.kt b/app/src/main/java/com/idormy/sms/forwarder/workers/BluetoothWorker.kt
new file mode 100644
index 00000000..66bf42b5
--- /dev/null
+++ b/app/src/main/java/com/idormy/sms/forwarder/workers/BluetoothWorker.kt
@@ -0,0 +1,115 @@
+package com.idormy.sms.forwarder.workers
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import com.google.gson.Gson
+import com.idormy.sms.forwarder.core.Core
+import com.idormy.sms.forwarder.entity.MsgInfo
+import com.idormy.sms.forwarder.entity.TaskSetting
+import com.idormy.sms.forwarder.entity.condition.BluetoothSetting
+import com.idormy.sms.forwarder.utils.Log
+import com.idormy.sms.forwarder.utils.TaskWorker
+import com.idormy.sms.forwarder.utils.task.ConditionUtils
+import java.util.Date
+
+@Suppress("PrivatePropertyName", "DEPRECATION")
+class BluetoothWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
+
+ private val TAG: String = BluetoothWorker::class.java.simpleName
+
+ override suspend fun doWork(): Result {
+ try {
+ val conditionType = inputData.getInt(TaskWorker.CONDITION_TYPE, -1)
+ val action = inputData.getString(TaskWorker.ACTION) ?: BluetoothAdapter.ACTION_STATE_CHANGED
+ val msg = inputData.getString(TaskWorker.MSG) ?: "1"
+ val taskList = Core.task.getByType(conditionType)
+ for (task in taskList) {
+ Log.d(TAG, "task = $task")
+
+ // 根据任务信息执行相应操作
+ val conditionList = Gson().fromJson(task.conditions, Array::class.java).toMutableList()
+ if (conditionList.isEmpty()) {
+ Log.d(TAG, "TASK-${task.id}:conditionList is empty")
+ continue
+ }
+ val firstCondition = conditionList.firstOrNull()
+ if (firstCondition == null) {
+ Log.d(TAG, "TASK-${task.id}:firstCondition is null")
+ continue
+ }
+
+ val bluetoothSetting = Gson().fromJson(firstCondition.setting, BluetoothSetting::class.java)
+ if (bluetoothSetting == null) {
+ Log.d(TAG, "TASK-${task.id}:bluetoothSetting is null")
+ continue
+ }
+
+ if (action != bluetoothSetting.action) {
+ Log.d(TAG, "TASK-${task.id}:action is not match, bluetoothSetting = $bluetoothSetting")
+ continue
+ }
+
+ var content = ""
+ when (action) {
+ BluetoothAdapter.ACTION_STATE_CHANGED -> {
+ if (msg != bluetoothSetting.state.toString()) {
+ Log.d(TAG, "TASK-${task.id}:bluetoothState is not match, bluetoothSetting = $bluetoothSetting")
+ continue
+ }
+ }
+
+ BluetoothDevice.ACTION_ACL_CONNECTED, BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
+ val devices = Gson().fromJson(msg, MutableMap::class.java)
+ Log.d(TAG, "TASK-${task.id}:devices = $devices")
+ if (devices.isEmpty() || !devices.containsKey(bluetoothSetting.device)) {
+ Log.d(TAG, "TASK-${task.id}:device is not match, bluetoothSetting = $bluetoothSetting")
+ continue
+ }
+ for ((k, v) in devices) {
+ content += "$k ($v)\n"
+ }
+ }
+
+ BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
+ val devices = Gson().fromJson(msg, MutableMap::class.java)
+ Log.d(TAG, "TASK-${task.id}:devices = $devices")
+ if (bluetoothSetting.result == 1 && !devices.containsKey(bluetoothSetting.device)) {
+ Log.d(TAG, "TASK-${task.id}:device is not discovered, bluetoothSetting = $bluetoothSetting")
+ continue
+ } else if (bluetoothSetting.result == 0 && devices.containsKey(bluetoothSetting.device)) {
+ Log.d(TAG, "TASK-${task.id}:device is discovered, bluetoothSetting = $bluetoothSetting")
+ continue
+ }
+ for ((k, v) in devices) {
+ content += "$k ($v)\n"
+ }
+ }
+ }
+
+ //TODO:判断其他条件是否满足
+ if (!ConditionUtils.checkCondition(task.id, conditionList)) {
+ Log.d(TAG, "TASK-${task.id}:other condition is not satisfied")
+ continue
+ }
+
+ //TODO: 组装消息体 && 执行具体任务
+ val msgInfo = MsgInfo("task", task.name, content.trim(), Date(), task.description)
+ val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, task.id).putString(TaskWorker.TASK_ACTIONS, task.actions).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
+ val actionRequest = OneTimeWorkRequestBuilder().setInputData(actionData).build()
+ WorkManager.getInstance().enqueue(actionRequest)
+ }
+
+ return Result.success()
+ } catch (e: Exception) {
+ Log.e(TAG, "Error running worker: ${e.message}", e)
+ return Result.failure()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/auto_task_icon_bluetooth.xml b/app/src/main/res/drawable/auto_task_icon_bluetooth.xml
new file mode 100644
index 00000000..bc2c38b2
--- /dev/null
+++ b/app/src/main/res/drawable/auto_task_icon_bluetooth.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/auto_task_icon_bluetooth_grey.xml b/app/src/main/res/drawable/auto_task_icon_bluetooth_grey.xml
new file mode 100644
index 00000000..00498f06
--- /dev/null
+++ b/app/src/main/res/drawable/auto_task_icon_bluetooth_grey.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_bluetooth.xml b/app/src/main/res/drawable/ic_bt_bluetooth.xml
new file mode 100644
index 00000000..55189ad5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_bluetooth.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_cellphone.xml b/app/src/main/res/drawable/ic_bt_cellphone.xml
new file mode 100644
index 00000000..15754f99
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_cellphone.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_headphones.xml b/app/src/main/res/drawable/ic_bt_headphones.xml
new file mode 100644
index 00000000..e234e92a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_headphones.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_headset_hfp.xml b/app/src/main/res/drawable/ic_bt_headset_hfp.xml
new file mode 100644
index 00000000..2abb7805
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_headset_hfp.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_imaging.xml b/app/src/main/res/drawable/ic_bt_imaging.xml
new file mode 100644
index 00000000..ab4cff70
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_imaging.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_laptop.xml b/app/src/main/res/drawable/ic_bt_laptop.xml
new file mode 100644
index 00000000..8e8cd957
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_laptop.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_misc_hid.xml b/app/src/main/res/drawable/ic_bt_misc_hid.xml
new file mode 100644
index 00000000..cc79e79a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_misc_hid.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_network_pan.xml b/app/src/main/res/drawable/ic_bt_network_pan.xml
new file mode 100644
index 00000000..d07fbcce
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_network_pan.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bt_wristband.xml b/app/src/main/res/drawable/ic_bt_wristband.xml
new file mode 100644
index 00000000..b8a86928
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bt_wristband.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/adapter_bluetooth_list_item.xml b/app/src/main/res/layout/adapter_bluetooth_list_item.xml
new file mode 100644
index 00000000..e05b05e2
--- /dev/null
+++ b/app/src/main/res/layout/adapter_bluetooth_list_item.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml
index 17e74a9b..277a7dc9 100644
--- a/app/src/main/res/layout/fragment_settings.xml
+++ b/app/src/main/res/layout/fragment_settings.xml
@@ -343,6 +343,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index d851a168..92291d6e 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -1063,8 +1063,8 @@
Broadcast Address, eg. 192.168.1.255
Malformed IP address, eg. 192.168.168.168
^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\.){3}(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])$
- Network card mac, eg. AA:BB:CC:DD:EE:FF
- The network card mac format is incorrect, eg. AA:BB:CC:DD:EE:FF
+ Required, eg. AA:BB:CC:DD:EE:FF
+ Mac format is incorrect, eg. AA:BB:CC:DD:EE:FF
^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$
Broadcast Address
IP
@@ -1155,6 +1155,12 @@
m
UID
+ Enable Bluetooth discovery
+ Bluetooth device discovery service must be enabled to proceed with retrieval!\nEnable now?
+ To support features like automatic tasks that require Bluetooth discovery
+ Scan Interval
+ Ignore Anonymous
+
Name/Status
Task Name
Description
@@ -1400,10 +1406,26 @@
Alarm Volume
Play Times(0=Infinite)
- %s tag is invalid: %s
+ %s tag is invalid: %s
Please input task name.
Please add trigger conditions.
Please add execution actions.
Please set the time for the scheduled task
Proxy server hostname resolution failed: proxyHost=%s
+
+ Bluetooth State Changed
+ Spec. St.
+ On
+ Off
+ Bluetooth Device Discovery Finished
+ Spec. Res.
+ Discovered
+ Undiscovered
+ Bluetooth Device Connected
+ Bluetooth Device Disconnected
+ Spec. Dev.
+ Bluetooth Device MAC Address
+ Bluetooth not supported.
+ Discovery
+ Bluetooth Mac Address is invalid, eg. AA:BB:CC:DD:EE:FF
diff --git a/app/src/main/res/values-en/styles_widget.xml b/app/src/main/res/values-en/styles_widget.xml
index c481a9aa..6e347eda 100644
--- a/app/src/main/res/values-en/styles_widget.xml
+++ b/app/src/main/res/values-en/styles_widget.xml
@@ -168,6 +168,7 @@
- 5dp
- match_parent
- wrap_content
+ - 30dp
- true
- @drawable/custom_radio_button
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index ba77b677..3b9b3cf4 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -1064,8 +1064,8 @@
可选,内网广播地址,例如:192.168.1.255
IP地址格式错误,例如:192.168.168.168
^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\.){3}(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])$
- 必填,网卡mac,例如:AA:BB:CC:DD:EE:FF
- 网卡mac格式错误,例如:AA:BB:CC:DD:EE:FF
+ 必填,例如:AA:BB:CC:DD:EE:FF
+ 格式错误,例如:AA:BB:CC:DD:EE:FF
^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$
内网广播地址
IP
@@ -1156,6 +1156,12 @@
米
UID
+ 发现蓝牙设备服务
+ 必须开启发现蓝牙设备服务,才能使用获取!\n是否立即启用?
+ 以便支持 自动任务 等需要发现蓝牙的功能
+ 扫描间隔
+ 忽略匿名设备
+
任务名称/状态
任务名称
任务描述
@@ -1401,10 +1407,26 @@
播放音量
播放次数(0=无限)
- %s 标签无效:%s
+ %s 标签无效:%s
请输入任务名称
请添加触发条件
请添加执行动作
请设置定时任务的时间
代理服务器主机名解析失败:proxyHost=%s
+
+ 蓝牙状态变化
+ 指定状态
+ 已打开
+ 已关闭
+ 蓝牙设备搜索完成
+ 指定结果
+ 已发现
+ 未发现
+ 蓝牙设备已连接
+ 蓝牙设备已断开
+ 指定设备
+ 蓝牙设备MAC地址
+ 不支持蓝牙设备
+ 搜索设备
+ 蓝牙设备MAC地址无效,例如:AA:BB:CC:DD:EE:FF
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 855716bf..62ac21a3 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -1064,8 +1064,8 @@
可選,內網廣播地址,例如:192.168.1.255
IP地址格式錯誤,例如:192.168.168.168
^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])$
- 必填,網卡mac,例如:AA:BB:CC:DD:EE:FF
- 網卡mac格式錯誤,例如:AA:BB:CC:DD:EE:FF
+ 必填,例如:AA:BB:CC:DD:EE:FF
+ 格式錯誤,例如:AA:BB:CC:DD:EE:FF
^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$
內網廣播地址
IP
@@ -1156,6 +1156,12 @@
米
UID
+ 啟用藍牙發現
+ 必須啟用藍牙裝置發現服務才能繼續獲取!\n現在啟用嗎?
+ 以支援自動任務等需要藍牙發現功能的功能
+ 掃描間隔
+ 忽略匿名裝置
+
任務名稱/狀態
任務名稱
任務描述
@@ -1401,10 +1407,26 @@
播放音量
播放次數(0=無限)
- %s 標籤無效:%s
+ %s 標籤無效:%s
請輸入任務名稱
請添加觸發條件
請添加執行動作
請設置定時任務的時間
代理伺服器主機名解析失敗:proxyHost=%s
+
+ 藍牙狀態變化
+ 指定狀態
+ 已開啟
+ 已關閉
+ 藍牙裝置搜索完成
+ 指定結果
+ 已發現
+ 未發現
+ 藍牙裝置已連接
+ 藍牙裝置已斷開
+ 指定裝置
+ 藍牙裝置MAC地址
+ 不支援藍牙裝置
+ 搜索裝置
+ 藍牙裝置MAC地址無效,例如:AA:BB:CC:DD:EE:FF
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3c4bd215..8d9c790c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1064,8 +1064,8 @@
可选,内网广播地址,例如:192.168.1.255
IP地址格式错误,例如:192.168.168.168
^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\.){3}(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])$
- 必填,网卡mac,例如:AA:BB:CC:DD:EE:FF
- 网卡mac格式错误,例如:AA:BB:CC:DD:EE:FF
+ 必填,例如:AA:BB:CC:DD:EE:FF
+ 格式错误,例如:AA:BB:CC:DD:EE:FF
^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$
内网广播地址
IP
@@ -1156,6 +1156,12 @@
米
UID
+ 发现蓝牙设备服务
+ 必须开启发现蓝牙设备服务,才能使用获取!\n是否立即启用?
+ 以便支持 自动任务 等需要发现蓝牙的功能
+ 扫描间隔
+ 忽略匿名设备
+
任务名称/状态
任务名称
任务描述
@@ -1401,10 +1407,26 @@
播放音量
播放次数(0=无限)
- %s 标签无效:%s
+ %s 标签无效:%s
请输入任务名称
请添加触发条件
请添加执行动作
请设置定时任务的时间
代理服务器主机名解析失败:proxyHost=%s
+
+ 蓝牙状态变化
+ 指定状态
+ 已打开
+ 已关闭
+ 蓝牙设备搜索完成
+ 指定结果
+ 已发现
+ 未发现
+ 蓝牙设备已连接
+ 蓝牙设备已断开
+ 指定设备
+ 蓝牙设备MAC地址
+ 不支持蓝牙设备
+ 搜索设备
+ 蓝牙设备MAC地址无效,例如:AA:BB:CC:DD:EE:FF
diff --git a/app/src/main/res/values/styles_widget.xml b/app/src/main/res/values/styles_widget.xml
index 4d3af7ec..17686435 100644
--- a/app/src/main/res/values/styles_widget.xml
+++ b/app/src/main/res/values/styles_widget.xml
@@ -170,6 +170,7 @@
- 5dp
- match_parent
- wrap_content
+ - 30dp
- true
- @drawable/custom_radio_button