新增:自动任务·快捷指令 —— 触发条件:蓝牙设备(状态变化、设备发现、连接断开) #388

pull/436/head
pppscn 3 months ago
parent e4432cb037
commit ad79f5e445

@ -67,6 +67,11 @@
<uses-permission
android:name="android.permission.REBOOT"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<application
android:name=".App"
@ -101,7 +106,7 @@
android:taskAffinity=":splash"
android:theme="@style/AppTheme.Launch.App"
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="TranslucentOrientation">
tools:ignore="DiscouragedApi,TranslucentOrientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -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" />
<activity
android:name=".activity.ClientActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<activity
android:name=".activity.TaskActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!--通用浏览器-->
<activity
android:name=".core.webview.AgentWebActivity"
@ -185,26 +193,30 @@
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!-- 版本更新提示-->
<activity
android:name=".utils.update.UpdateTipDialog"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/DialogTheme" />
android:theme="@style/DialogTheme"
tools:ignore="DiscouragedApi" />
<!-- Webview拦截提示弹窗-->
<activity
android:name=".core.webview.WebViewInterceptDialog"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/DialogTheme" />
android:theme="@style/DialogTheme"
tools:ignore="DiscouragedApi" />
<!-- applink的中转页面 -->
<activity
android:name=".core.XPageTransferActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!--屏幕自适应设计图-->
<meta-data
@ -216,6 +228,10 @@
android:exported="true"
android:value="640" />
<service
android:name=".service.BluetoothScanService"
android:enabled="true"
android:exported="false" />
<service
android:name=".service.ForegroundService"
android:enabled="true" />
@ -244,6 +260,30 @@
<action android:name="android.intent.action.BATTERY_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BluetoothReceiver"
android:exported="true">
<intent-filter>
<!-- 蓝牙设备发现 -->
<action android:name="android.bluetooth.device.action.FOUND" />
<!-- 蓝牙扫描完成 -->
<action android:name="android.bluetooth.adapter.action.DISCOVERY_FINISHED" />
<!-- 蓝牙状态改变 -->
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<!-- 蓝牙扫描模式改变 -->
<action android:name="android.bluetooth.adapter.action.SCAN_MODE_CHANGED" />
<!-- 本地蓝牙名称改变 -->
<action android:name="android.bluetooth.adapter.action.LOCAL_NAME_CHANGED" />
<!-- 蓝牙连接状态改变 -->
<action android:name="android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED" />
<!-- 蓝牙设备配对状态改变 -->
<action android:name="android.bluetooth.device.action.BOND_STATE_CHANGED" />
<!-- 蓝牙设备连接 -->
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
<!-- 蓝牙设备断开连接 -->
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BootCompletedReceiver"
android:defaultToDeviceProtectedStorage="true"

@ -3,6 +3,8 @@ package com.idormy.sms.forwarder
import android.annotation.SuppressLint
import android.app.Application
import android.app.PendingIntent
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@ -30,12 +32,15 @@ import com.idormy.sms.forwarder.database.repository.SenderRepository
import com.idormy.sms.forwarder.database.repository.TaskRepository
import com.idormy.sms.forwarder.entity.SimInfo
import com.idormy.sms.forwarder.receiver.BatteryReceiver
import com.idormy.sms.forwarder.receiver.BluetoothReceiver
import com.idormy.sms.forwarder.receiver.CactusReceiver
import com.idormy.sms.forwarder.receiver.LockScreenReceiver
import com.idormy.sms.forwarder.receiver.NetworkChangeReceiver
import com.idormy.sms.forwarder.service.BluetoothScanService
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.service.HttpServerService
import com.idormy.sms.forwarder.service.LocationService
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.AppInfo
import com.idormy.sms.forwarder.utils.CactusSave
import com.idormy.sms.forwarder.utils.FRONT_CHANNEL_ID
@ -185,7 +190,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
//启动前台服务
val foregroundServiceIntent = Intent(this, ForegroundService::class.java)
foregroundServiceIntent.action = "START"
foregroundServiceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= 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 {

@ -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<ActivityMainBinding?>(), 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 {

@ -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<BluetoothDevice>,
private var itemClickListener: ((Int) -> Unit)? = null,
private var removeClickListener: ((Int) -> Unit)? = null,
private var editClickListener: ((Int) -> Unit)? = null,
) : RecyclerView.Adapter<BluetoothRecyclerAdapter.ViewHolder>() {
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
}
}

@ -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
}
}
}

@ -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<FragmentFrpcsBinding?>(), 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 {

@ -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<FragmentServerBinding?>(), View.OnClickListe
}
//重启前台服务,启动/停止定位服务
val serviceIntent = Intent(requireContext(), LocationService::class.java)
serviceIntent.action = "RESTART"
serviceIntent.action = ACTION_RESTART
requireContext().startService(serviceIntent)
}

@ -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<FragmentSettingsBinding?>(), 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<FragmentSettingsBinding?>(), 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<String>, all: Boolean) {
try {
@ -352,7 +361,6 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), 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<FragmentSettingsBinding?>(), View.OnClickL
}
SettingUtils.enablePhone = isChecked
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 读取电话状态
.permission(Permission.READ_PHONE_STATE)
@ -499,7 +506,6 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), 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<FragmentSettingsBinding?>(), 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<String>, 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<String>, 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<FragmentSettingsBinding?>(), 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<String>, never: Boolean) {
@ -561,11 +645,11 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), 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<FragmentSettingsBinding?>(), 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<FragmentSettingsBinding?>(), 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<FragmentSettingsBinding?>(), 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<FragmentSettingsBinding?>(), 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) }
}
})

@ -82,56 +82,56 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), 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<FragmentTasksEditBinding?>(), 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<FragmentTasksEditBinding?>(), 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

@ -164,7 +164,6 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), 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<FragmentTasksActionSettingsBinding?>(), Vi
// 发送短信
//.permission(Permission.SEND_SMS)
// 读取短信
.permission(Permission.READ_SMS).request(object : OnPermissionCallback {
.permission(Permission.READ_SMS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
@ -200,7 +200,6 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
binding!!.sbEnablePhone.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 读取电话状态
.permission(Permission.READ_PHONE_STATE)
@ -209,7 +208,8 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), 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<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
@ -234,37 +234,42 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), 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<String>, 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<String>, all: Boolean) {
}
override fun onDenied(permissions: List<String>, 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<String>, 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<FragmentTasksActionSettingsBinding?>(), Vi
binding!!.sbEnableSmsCommand.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 系统设置
.permission(Permission.WRITE_SETTINGS)
@ -305,7 +309,8 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
// 发送短信
.permission(Permission.SEND_SMS)
// 读取短信
.permission(Permission.READ_SMS).request(object : OnPermissionCallback {
.permission(Permission.READ_SMS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)

@ -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<FragmentTasksConditionBluetoothBinding?>(), 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<BluetoothDevice> = 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<String>, 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<String>, 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
}
}

@ -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<FragmentTasksConditionLeaveAddressBind
when (v.id) {
R.id.btn_current_coordinates -> {
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
}

@ -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<FragmentTasksConditionToAddressBinding?>(
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

@ -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>(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>(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>(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<BluetoothWorker>()
.setInputData(
workDataOf(
TaskWorker.CONDITION_TYPE to TASK_CONDITION_BLUETOOTH,
TaskWorker.ACTION to action,
TaskWorker.MSG to msg,
)
).build()
WorkManager.getInstance(context).enqueue(request)
}
}

@ -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()
}
}

@ -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)

@ -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
}

@ -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)
}
}

@ -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

@ -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 {

@ -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-$taskIdbluetoothSetting is null")
continue
}
when (bluetoothSetting.action) {
BluetoothAdapter.ACTION_STATE_CHANGED -> {
if (TaskUtils.bluetoothState != bluetoothSetting.state) {
Log.d(TAG, "TASK-$taskIdbluetoothState is not match, bluetoothSetting = $bluetoothSetting")
return false
}
}
BluetoothDevice.ACTION_ACL_CONNECTED -> {
if (!TaskUtils.connectedDevices.containsKey(bluetoothSetting.device)) {
Log.d(TAG, "TASK-$taskIddevice is not connected, bluetoothSetting = $bluetoothSetting")
return false
}
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
if (TaskUtils.connectedDevices.containsKey(bluetoothSetting.device)) {
Log.d(TAG, "TASK-$taskIddevice is connected, bluetoothSetting = $bluetoothSetting")
return false
}
}
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
if (bluetoothSetting.result == 1 && !TaskUtils.discoveredDevices.containsKey(bluetoothSetting.device)) {
Log.d(TAG, "TASK-$taskIddevice is not discovered, bluetoothSetting = $bluetoothSetting")
return false
} else if (bluetoothSetting.result == 0 && TaskUtils.discoveredDevices.containsKey(bluetoothSetting.device)) {
Log.d(TAG, "TASK-$taskIddevice is discovered, bluetoothSetting = $bluetoothSetting")
return false
}
}
}
Log.d(TAG, "TASK-$taskIdbluetoothAction is match, bluetoothSetting = $bluetoothSetting")
}
}
}

@ -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<String, String> by SharedPreference(SP_DISCOVERED_DEVICES, mutableMapOf())
//已连接的蓝牙设备
var connectedDevices: MutableMap<String, String> by SharedPreference(SP_CONNECTED_DEVICE, mutableMapOf())
//蓝牙状态
var bluetoothState: Int by SharedPreference(SP_BLUETOOTH_STATE, BluetoothAdapter.STATE_ON)
}
}

@ -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)
}

@ -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<TaskSetting>::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<ActionWorker>().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()
}
}
}

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25.0dip"
android:height="25.0dip"
android:autoMirrored="true"
android:viewportWidth="25.0"
android:viewportHeight="25.0">
<path
android:fillColor="#ff2eafff"
android:pathData="M6.66,0.66L18.66,0.66A6,6 0,0 1,24.66 6.66L24.66,18.66A6,6 0,0 1,18.66 24.66L6.66,24.66A6,6 0,0 1,0.66 18.66L0.66,6.66A6,6 0,0 1,6.66 0.66z" />
<path
android:fillColor="#ffffffff"
android:fillType="evenOdd"
android:pathData="M13.285,5.328C12.461,4.618 11.189,5.207 11.189,6.298V8.095C11.189,8.1 11.188,8.105 11.188,8.11V10.365L7.469,8.362C7.035,8.11 6.48,8.26 6.23,8.696C5.979,9.133 6.128,9.691 6.562,9.942L10.845,12.284L6.563,14.625C6.128,14.876 5.979,15.434 6.23,15.871C6.48,16.307 7.035,16.457 7.469,16.205L11.188,14.202V18.269C11.188,19.36 12.461,19.949 13.285,19.239L17.651,15.773C18.317,15.2 18.219,14.137 17.459,13.696L14.697,12.283L17.46,10.871C18.219,10.43 18.317,9.367 17.652,8.794L13.285,5.328ZM13.003,16.472C13.003,16.467 13.003,16.462 13.003,16.457V13.523L15.887,14.891L13.003,17.079V16.472ZM15.887,9.676L13.003,11.044V7.488L15.887,9.676Z" />
</vector>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="25.0dip"
android:height="25.0dip"
android:autoMirrored="true"
android:viewportWidth="25.0"
android:viewportHeight="25.0">
<path
android:fillColor="#ffe6e6e6"
android:pathData="M6.66,0.66L18.66,0.66A6,6 0,0 1,24.66 6.66L24.66,18.66A6,6 0,0 1,18.66 24.66L6.66,24.66A6,6 0,0 1,0.66 18.66L0.66,6.66A6,6 0,0 1,6.66 0.66z" />
<path
android:fillColor="#ffffffff"
android:fillType="evenOdd"
android:pathData="M13.285,5.328C12.461,4.618 11.189,5.207 11.189,6.298V8.095C11.189,8.1 11.188,8.105 11.188,8.11V10.365L7.469,8.362C7.035,8.11 6.48,8.26 6.23,8.696C5.979,9.133 6.128,9.691 6.562,9.942L10.845,12.284L6.563,14.625C6.128,14.876 5.979,15.434 6.23,15.871C6.48,16.307 7.035,16.457 7.469,16.205L11.188,14.202V18.269C11.188,19.36 12.461,19.949 13.285,19.239L17.651,15.773C18.317,15.2 18.219,14.137 17.459,13.696L14.697,12.283L17.46,10.871C18.219,10.43 18.317,9.367 17.652,8.794L13.285,5.328ZM13.003,16.472C13.003,16.467 13.003,16.462 13.003,16.457V13.523L15.887,14.891L13.003,17.079V16.472ZM15.887,9.676L13.003,11.044V7.488L15.887,9.676Z" />
</vector>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#ff99a1bd"
android:pathData="M0,38C0,17.0132 17.0132,0 38,0V0C58.9868,0 76,17.0132 76,38V38C76,58.9868 58.9868,76 38,76V76C17.0132,76 0,58.9868 0,38V38Z" />
<path
android:fillColor="#00000000"
android:pathData="M23,32.3444L35.8889,37.855V22L52,30.6042L36.1574,38.0483L52,45.4924L35.8889,54V38.145L23,43.6556"
android:strokeWidth="4.0"
android:strokeColor="#ffffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#ff13d4b2"
android:pathData="M0,38C0,17.0132 17.0132,0 38,0V0C58.9868,0 76,17.0132 76,38V38C76,58.9868 58.9868,76 38,76V76C17.0132,76 0,58.9868 0,38V38Z" />
<path
android:fillColor="#00000000"
android:pathData="M31,21L45,21A6,6 0,0 1,51 27L51,49A6,6 0,0 1,45 55L31,55A6,6 0,0 1,25 49L25,27A6,6 0,0 1,31 21z"
android:strokeWidth="4.0"
android:strokeColor="#ffffffff" />
</vector>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#ff2eafff"
android:pathData="M0,38C0,17.0132 17.0132,0 38,0V0C58.9868,0 76,17.0132 76,38V38C76,58.9868 58.9868,76 38,76V76C17.0132,76 0,58.9868 0,38V38Z" />
<path
android:fillColor="#ffffffff"
android:fillType="evenOdd"
android:pathData="M38,19C30.268,19 24,25.268 24,33V49C24,50.1046 24.8954,51 26,51C27.1046,51 28,50.1046 28,49V33C28,27.4772 32.4772,23 38,23C43.5228,23 48,27.4772 48,33V49C48,50.1046 48.8954,51 50,51C51.1046,51 52,50.1046 52,49V33C52,25.268 45.732,19 38,19ZM20,39C21.1046,39 22,39.8954 22,41L22,49C22,50.1046 21.1046,51 20,51C18.8954,51 18,50.1046 18,49V41C18,39.8954 18.8954,39 20,39ZM56,39C57.1046,39 58,39.8954 58,41V49C58,50.1046 57.1046,51 56,51C54.8954,51 54,50.1046 54,49V41C54,39.8954 54.8954,39 56,39Z" />
</vector>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#ff2eafff"
android:pathData="M0,38C0,17.0132 17.0132,0 38,0V0C58.9868,0 76,17.0132 76,38V38C76,58.9868 58.9868,76 38,76V76C17.0132,76 0,58.9868 0,38V38Z" />
<path
android:fillColor="#ffffffff"
android:fillType="evenOdd"
android:pathData="M38,19C30.268,19 24,25.268 24,33V49C24,50.1046 24.8954,51 26,51C27.1046,51 28,50.1046 28,49V33C28,27.4772 32.4772,23 38,23C43.5228,23 48,27.4772 48,33V45C48,46.1046 47.1046,47 46,47H40C38.8954,47 38,47.8954 38,49C38,50.1046 38.8954,51 40,51H46C49.3137,51 52,48.3137 52,45V33C52,25.268 45.732,19 38,19ZM20,39C21.1046,39 22,39.8954 22,41L22,49C22,50.1046 21.1046,51 20,51C18.8954,51 18,50.1046 18,49V41C18,39.8954 18.8954,39 20,39ZM56,39C57.1046,39 58,39.8954 58,41V49C58,50.1046 57.1046,51 56,51C54.8954,51 54,50.1046 54,49V41C54,39.8954 54.8954,39 56,39Z" />
</vector>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#fffa9a2a"
android:pathData="M0,38C0,17.0132 17.0132,0 38,0V0C58.9868,0 76,17.0132 76,38V38C76,58.9868 58.9868,76 38,76V76C17.0132,76 0,58.9868 0,38V38Z" />
<path
android:fillColor="#00000000"
android:pathData="M20,46L20,32A4,4 0,0 1,24 28L52,28A4,4 0,0 1,56 32L56,46A4,4 0,0 1,52 50L24,50A4,4 0,0 1,20 46z"
android:strokeWidth="4.0"
android:strokeColor="#ffffffff" />
<path
android:fillColor="#00000000"
android:pathData="M29,28L29,24.8C29,23.1198 29,22.2798 29.327,21.638C29.6146,21.0735 30.0735,20.6146 30.638,20.327C31.2798,20 32.1198,20 33.8,20L42.2,20C43.8802,20 44.7202,20 45.362,20.327C45.9265,20.6146 46.3854,21.0735 46.673,21.638C47,22.2798 47,23.1198 47,24.8L47,28L29,28Z"
android:strokeWidth="4.0"
android:strokeColor="#ffffffff" />
<path
android:fillColor="#fffa9a2a"
android:pathData="M33.8,57C32.1198,57 31.2798,57 30.638,56.673C30.0735,56.3854 29.6146,55.9265 29.327,55.362C29,54.7202 29,53.8802 29,52.2L29,40L47,40L47,52.2C47,53.8802 47,54.7202 46.673,55.362C46.3854,55.9265 45.9265,56.3854 45.362,56.673C44.7202,57 43.8802,57 42.2,57L33.8,57Z"
android:strokeWidth="4.0"
android:strokeColor="#ffffffff" />
</vector>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#ff13d4b2"
android:pathData="M0,38C0,17.0132 17.0132,0 38,0V0C58.9868,0 76,17.0132 76,38V38C76,58.9868 58.9868,76 38,76V76C17.0132,76 0,58.9868 0,38V38Z" />
<path
android:fillColor="#ffffffff"
android:fillType="evenOdd"
android:pathData="M29.48,23L29.4003,23L29.4003,23C28.3105,23 27.3704,22.9999 26.5969,23.0652C25.78,23.1341 24.9686,23.2865 24.1924,23.6974C23.1295,24.2602 22.2602,25.1295 21.6974,26.1924C21.2864,26.9686 21.1341,27.78 21.0652,28.5969C20.9999,29.3704 21,30.3105 21,31.4002V31.4003L21,31.48V44.4286L18.5535,46.7586L18.4972,46.8122L18.4971,46.8123C18.0787,47.2106 17.6711,47.5988 17.373,47.9391C17.1115,48.2377 16.5727,48.8811 16.5077,49.7848C16.4417,50.7028 16.8007,51.6003 17.4816,52.2195C18.152,52.8291 18.9858,52.9235 19.3811,52.9593C19.8316,53.0002 20.3945,53.0001 20.9723,53L21.05,53H54.95L55.0278,53C55.6055,53.0001 56.1684,53.0002 56.619,52.9593C57.0142,52.9235 57.8481,52.8291 58.5184,52.2195C59.1993,51.6003 59.5583,50.7028 59.4923,49.7848C59.4273,48.8811 58.8885,48.2377 58.627,47.9391C58.329,47.5988 57.9213,47.2106 57.5029,46.8123L57.4466,46.7586L55,44.4286V31.48V31.4003V31.4002C55,30.3104 55.0001,29.3704 54.9348,28.5969C54.8659,27.78 54.7136,26.9686 54.3026,26.1924C53.7398,25.1295 52.8705,24.2602 51.8076,23.6974C51.0314,23.2865 50.22,23.1341 49.4031,23.0652C48.6296,22.9999 47.6895,23 46.5997,23L46.52,23H29.48ZM26.0641,27.2325C26.1661,27.1785 26.3727,27.0983 26.9332,27.051C27.5183,27.0017 28.288,27 29.48,27H46.52C47.712,27 48.4817,27.0017 49.0668,27.051C49.6273,27.0983 49.8339,27.1785 49.9359,27.2325C50.2902,27.4201 50.5799,27.7098 50.7675,28.0641C50.8215,28.1661 50.9017,28.3727 50.949,28.9332C50.9983,29.5183 51,30.288 51,31.48V43H25V31.48C25,30.288 25.0017,29.5183 25.051,28.9332C25.0983,28.3727 25.1785,28.1661 25.2325,28.0641C25.4201,27.7098 25.7098,27.4201 26.0641,27.2325ZM24.1,47L22,49H54L51.9,47H24.1Z" />
</vector>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#ff565bdf"
android:pathData="M0,38C0,17.0132 17.0132,0 38,0V0C58.9868,0 76,17.0132 76,38V38C76,58.9868 58.9868,76 38,76V76C17.0132,76 0,58.9868 0,38V38Z" />
<path
android:fillColor="#ffffffff"
android:fillType="evenOdd"
android:pathData="M46.5,32.5C46.5,33.6046 45.6046,34.5 44.5,34.5C43.3954,34.5 42.5,33.6046 42.5,32.5C42.5,31.3954 43.3954,30.5 44.5,30.5C45.6046,30.5 46.5,31.3954 46.5,32.5ZM42.5,36.5C42.5,37.6046 41.6046,38.5 40.5,38.5C39.3954,38.5 38.5,37.6046 38.5,36.5C38.5,35.3954 39.3954,34.5 40.5,34.5C41.6046,34.5 42.5,35.3954 42.5,36.5ZM48.5,38.5C49.6046,38.5 50.5,37.6046 50.5,36.5C50.5,35.3954 49.6046,34.5 48.5,34.5C47.3954,34.5 46.5,35.3954 46.5,36.5C46.5,37.6046 47.3954,38.5 48.5,38.5ZM46.5,40.5C46.5,41.6046 45.6046,42.5 44.5,42.5C43.3954,42.5 42.5,41.6046 42.5,40.5C42.5,39.3954 43.3954,38.5 44.5,38.5C45.6046,38.5 46.5,39.3954 46.5,40.5ZM30,31C30.8284,31 31.5,31.6716 31.5,32.5V35H34C34.8284,35 35.5,35.6716 35.5,36.5C35.5,37.3284 34.8284,38 34,38H31.5V40.5C31.5,41.3284 30.8284,42 30,42C29.1716,42 28.5,41.3284 28.5,40.5V38H26C25.1716,38 24.5,37.3284 24.5,36.5C24.5,35.6716 25.1716,35 26,35H28.5V32.5C28.5,31.6716 29.1716,31 30,31Z" />
<path
android:fillColor="#00000000"
android:pathData="M20,32.6C20,29.2397 20,27.5595 20.654,26.2761C21.2292,25.1471 22.1471,24.2292 23.2761,23.654C24.5595,23 26.2397,23 29.6,23H45.4C48.7603,23 50.4405,23 51.7239,23.654C52.8529,24.2292 53.7708,25.1471 54.346,26.2761C55,27.5595 55,29.2397 55,32.6V46.0043C55,49.3157 52.3157,52 49.0043,52V52C47.8588,52 46.7407,51.6685 45.7453,51.1016C43.6741,49.9223 39.9263,48 37.5,48C35.0737,48 31.3259,49.9223 29.2547,51.1016C28.2593,51.6685 27.1412,52 25.9957,52V52C22.6843,52 20,49.3157 20,46.0043V32.6Z"
android:strokeWidth="4.0"
android:strokeColor="#ffffffff" />
</vector>

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#ff99a1bd"
android:pathData="M0,38C0,17.0132 17.0132,0 38,0V0C58.9868,0 76,17.0132 76,38V38C76,58.9868 58.9868,76 38,76V76C17.0132,76 0,58.9868 0,38V38Z" />
<path
android:fillColor="#00000000"
android:pathData="M23,32.3444L35.8889,37.855V22L52,30.6042L36.1574,38.0483L52,45.4924L35.8889,54V38.145L23,43.6556"
android:strokeWidth="4.0"
android:strokeColor="#ffffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#ffffffff"
android:pathData="M59.5,38.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
<path
android:fillColor="#ffffffff"
android:pathData="M14.5,38.5m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0" />
</vector>

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dip"
android:height="24.0dip"
android:viewportWidth="76.0"
android:viewportHeight="76.0">
<path
android:fillColor="#fff36537"
android:pathData="M38,0L38,0A38,38 0,0 1,76 38L76,38A38,38 0,0 1,38 76L38,76A38,38 0,0 1,0 38L0,38A38,38 0,0 1,38 0z" />
<path
android:fillColor="#00000000"
android:pathData="M51.2306,29C52.9783,31.5642 54,34.6628 54,38C54,46.8366 46.8366,54 38,54C29.1634,54 22,46.8366 22,38C22,34.6628 23.0217,31.5642 24.7694,29"
android:strokeWidth="4.0"
android:strokeColor="#ffffffff"
android:strokeLineCap="round" />
<path
android:fillColor="#ffffffff"
android:pathData="M28,22.6667C28,21.7462 28.7462,21 29.6667,21H46.3333C47.2538,21 48,21.7462 48,22.6667C48,24.5076 46.5076,26 44.6667,26H31.3333C29.4924,26 28,24.5076 28,22.6667Z" />
</vector>

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
android:background="@color/xui_config_color_white"
android:orientation="vertical"
tools:ignore="UseCompoundDrawables">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/xui_config_color_separator_light" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="@dimen/config_padding_5dp"
android:paddingBottom="@dimen/config_padding_5dp">
<ImageView
android:id="@+id/image_device_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:src="@drawable/auto_task_icon_bluetooth"
tools:ignore="ContentDescription" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/text_device_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/text_device_address"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/darker_gray"
android:textSize="12sp" />
</LinearLayout>
<ImageView
android:id="@+id/iv_edit"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="5dp"
android:src="@drawable/ic_edit"
app:tint="@color/toast_info_color"
tools:ignore="ContentDescription,PrivateResource,ImageContrastCheck" />
<ImageView
android:id="@+id/iv_remove"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_delete"
app:tint="@color/toast_error_color"
tools:ignore="ContentDescription,PrivateResource" />
</LinearLayout>
</LinearLayout>

@ -343,6 +343,118 @@
</LinearLayout>
<LinearLayout
style="@style/BarStyle.Switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enable_bluetooth"
android:textStyle="bold"
tools:ignore="RelativeOverlap" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/enable_bluetooth_tips"
android:textSize="@dimen/text_size_mini"
tools:ignore="SmallSp" />
</LinearLayout>
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
android:id="@+id/sb_enable_bluetooth"
style="@style/SwitchButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="TouchTargetSizeCheck,DuplicateSpeakableTextCheck" />
</LinearLayout>
<LinearLayout
android:id="@+id/layout_bluetooth_setting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/xui_config_color_separator_light" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/scan_interval"
android:textSize="@dimen/text_size_small"
android:textStyle="bold" />
<com.xuexiang.xui.widget.picker.XSeekBar
android:id="@+id/xsb_scan_interval"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:padding="0dp"
app:xsb_max="120"
app:xsb_min="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@string/seconds"
android:textSize="@dimen/text_size_small"
android:textStyle="bold" />
<com.xuexiang.xui.widget.button.SmoothCheckBox
android:id="@+id/scb_ignore_anonymous"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginStart="@dimen/config_margin_10dp"
app:scb_color_checked="@color/colorPrimary"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:singleLine="true"
android:text="@string/ignore_anonymous"
android:textSize="@dimen/text_size_small"
tools:ignore="SmallSp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
style="@style/BarStyle.Switch"
android:layout_width="match_parent"

@ -0,0 +1,258 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:background="?attr/xui_config_color_background"
android:orientation="vertical">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:overScrollMode="never">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:gravity="center_horizontal"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_margin="10dp"
android:contentDescription="@string/task_bluetooth"
app:srcCompat="@drawable/auto_task_icon_bluetooth"
tools:ignore="ImageContrastCheck" />
<LinearLayout
style="@style/BarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/task_bluetooth"
android:textSize="@dimen/text_size_big"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/task_bluetooth_tips"
android:textSize="@dimen/text_size_mini"
tools:ignore="SmallSp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="?attr/xui_config_color_separator_light" />
<RadioGroup
android:id="@+id/rg_bluetooth_action"
style="@style/rg_style"
android:orientation="vertical"
android:paddingBottom="@dimen/config_padding_5dp">
<RadioButton
android:id="@+id/rb_action_state_changed"
style="@style/rg_rb_style_match"
android:checked="true"
android:text="@string/bluetooth_state_changed"
tools:ignore="TouchTargetSizeCheck" />
<LinearLayout
android:id="@+id/layout_bluetooth_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="@dimen/config_margin_10dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/specified_state"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rg_bluetooth_state"
style="@style/rg_style"
android:layout_marginStart="15dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_state_on"
style="@style/rg_rb_style"
android:checked="true"
android:text="@string/state_on"
tools:ignore="TouchTargetSizeCheck" />
<RadioButton
android:id="@+id/rb_state_off"
style="@style/rg_rb_style"
android:text="@string/state_off"
tools:ignore="TouchTargetSizeCheck" />
</RadioGroup>
</LinearLayout>
<RadioButton
android:id="@+id/rb_action_discovery_finished"
style="@style/rg_rb_style_match"
android:text="@string/bluetooth_discovery_finished"
tools:ignore="TouchTargetSizeCheck" />
<LinearLayout
android:id="@+id/layout_discovery_finished"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="@dimen/config_margin_10dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/specified_result"
android:textStyle="bold" />
<RadioGroup
android:id="@+id/rg_discovery_result"
style="@style/rg_style"
android:layout_marginStart="15dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/rb_discovered"
style="@style/rg_rb_style"
android:checked="true"
android:text="@string/discovered"
tools:ignore="TouchTargetSizeCheck" />
<RadioButton
android:id="@+id/rb_undiscovered"
style="@style/rg_rb_style"
android:text="@string/undiscovered"
tools:ignore="TouchTargetSizeCheck" />
</RadioGroup>
</LinearLayout>
<RadioButton
android:id="@+id/rb_action_acl_connected"
style="@style/rg_rb_style_match"
android:text="@string/bluetooth_acl_connected"
tools:ignore="TouchTargetSizeCheck" />
<RadioButton
android:id="@+id/rb_action_acl_disconnected"
style="@style/rg_rb_style_match"
android:text="@string/bluetooth_acl_disconnected"
tools:ignore="TouchTargetSizeCheck" />
</RadioGroup>
<LinearLayout
android:id="@+id/layout_device_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginEnd="@dimen/config_margin_10dp"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="top"
android:orientation="horizontal"
tools:ignore="UselessParent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/specified_device"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
android:id="@+id/et_device_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/mac_hint"
android:singleLine="true"
app:met_clearButton="true"
app:met_errorMessage="@string/mac_error"
app:met_regexp="@string/mac_regex"
app:met_validateOnFocusLost="true" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/btn_start_discovery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:padding="5dp"
android:text="@string/start_discovery"
android:textColor="@color/white"
android:textSize="@dimen/text_size_mini"
app:sb_color_unpressed="@color/colorPrimary"
app:sb_ripple_color="@color/white"
app:sb_ripple_duration="500"
app:sb_shape_type="rectangle"
tools:ignore="SmallSp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_devices"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/config_margin_5dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:padding="10dp">
<com.xuexiang.xui.widget.textview.supertextview.SuperButton
android:id="@+id/btn_del"
style="@style/SuperButton.Gray.Icon.Spacing"
android:drawableStart="@drawable/ic_delete"
android:text="@string/discard"
tools:ignore="RtlSymmetry,TextContrastCheck,TouchTargetSizeCheck" />
<com.xuexiang.xui.widget.textview.supertextview.SuperButton
android:id="@+id/btn_save"
style="@style/SuperButton.Blue.Icon.Spacing"
android:drawableStart="@drawable/ic_save"
android:text="@string/submit"
tools:ignore="RtlSymmetry,TextContrastCheck,TouchTargetSizeCheck" />
</LinearLayout>
</LinearLayout>

@ -1063,8 +1063,8 @@
<string name="ip_hint">Broadcast Address, eg. 192.168.1.255</string>
<string name="ip_error">Malformed IP address, eg. 192.168.168.168</string>
<string name="ip_regex" tools:ignore="TypographyDashes">^((\\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])$</string>
<string name="mac_hint">Network card mac, eg. AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">The network card mac format is incorrect, eg. AA:BB:CC:DD:EE:FF</string>
<string name="mac_hint">Required, eg. AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">Mac format is incorrect, eg. AA:BB:CC:DD:EE:FF</string>
<string name="mac_regex" tools:ignore="TypographyDashes">^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$</string>
<string name="broadcast_address">Broadcast Address</string>
<string name="ip">IP</string>
@ -1155,6 +1155,12 @@
<string name="meter">m</string>
<string name="uid">UID</string>
<string name="enable_bluetooth">Enable Bluetooth discovery</string>
<string name="enable_bluetooth_dialog">Bluetooth device discovery service must be enabled to proceed with retrieval!\nEnable now?</string>
<string name="enable_bluetooth_tips">To support features like automatic tasks that require Bluetooth discovery</string>
<string name="scan_interval">Scan Interval</string>
<string name="ignore_anonymous">Ignore Anonymous</string>
<string name="task_name_status">Name/Status</string>
<string name="task_name">Task Name</string>
<string name="task_description">Description</string>
@ -1400,10 +1406,26 @@
<string name="alarm_volume">Alarm Volume</string>
<string name="alarm_play_times">Play Times(0=Infinite)</string>
<string name="invalid_tag">%s tag is invalid: %s</string>
<string name="invalid_tag" formatted="false">%s tag is invalid: %s</string>
<string name="invalid_task_name">Please input task name.</string>
<string name="invalid_conditions">Please add trigger conditions.</string>
<string name="invalid_actions">Please add execution actions.</string>
<string name="invalid_cron">Please set the time for the scheduled task</string>
<string name="invalid_proxy_host">Proxy server hostname resolution failed: proxyHost=%s</string>
<string name="bluetooth_state_changed">Bluetooth State Changed</string>
<string name="specified_state">Spec. St.</string>
<string name="state_on">On</string>
<string name="state_off">Off</string>
<string name="bluetooth_discovery_finished">Bluetooth Device Discovery Finished</string>
<string name="specified_result">Spec. Res.</string>
<string name="discovered">Discovered</string>
<string name="undiscovered">Undiscovered</string>
<string name="bluetooth_acl_connected">Bluetooth Device Connected</string>
<string name="bluetooth_acl_disconnected">Bluetooth Device Disconnected</string>
<string name="specified_device">Spec. Dev.</string>
<string name="device_address_hint">Bluetooth Device MAC Address</string>
<string name="bluetooth_not_supported">Bluetooth not supported.</string>
<string name="start_discovery">Discovery</string>
<string name="invalid_bluetooth_mac_address">Bluetooth Mac Address is invalid, eg. AA:BB:CC:DD:EE:FF</string>
</resources>

@ -168,6 +168,7 @@
<item name="android:layout_marginTop">5dp</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:minHeight">30dp</item>
<item name="android:singleLine">true</item>
<!--<item name="android:textSize">@dimen/text_size_small</item>-->
<item name="android:button">@drawable/custom_radio_button</item>

@ -1064,8 +1064,8 @@
<string name="ip_hint">可选内网广播地址例如192.168.1.255</string>
<string name="ip_error">IP地址格式错误例如192.168.168.168</string>
<string name="ip_regex" tools:ignore="TypographyDashes">^((\\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])$</string>
<string name="mac_hint">必填,网卡mac例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">网卡mac格式错误例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_hint">必填例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">格式错误例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_regex" tools:ignore="TypographyDashes">^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$</string>
<string name="broadcast_address">内网广播地址</string>
<string name="ip">IP</string>
@ -1156,6 +1156,12 @@
<string name="meter"></string>
<string name="uid">UID</string>
<string name="enable_bluetooth">发现蓝牙设备服务</string>
<string name="enable_bluetooth_dialog">必须开启发现蓝牙设备服务,才能使用获取!\n是否立即启用</string>
<string name="enable_bluetooth_tips">以便支持 自动任务 等需要发现蓝牙的功能</string>
<string name="scan_interval">扫描间隔</string>
<string name="ignore_anonymous">忽略匿名设备</string>
<string name="task_name_status">任务名称/状态</string>
<string name="task_name">任务名称</string>
<string name="task_description">任务描述</string>
@ -1401,10 +1407,26 @@
<string name="alarm_volume">播放音量</string>
<string name="alarm_play_times">播放次数(0=无限)</string>
<string name="invalid_tag">%s 标签无效:%s</string>
<string name="invalid_tag" formatted="false">%s 标签无效:%s</string>
<string name="invalid_task_name">请输入任务名称</string>
<string name="invalid_conditions">请添加触发条件</string>
<string name="invalid_actions">请添加执行动作</string>
<string name="invalid_cron">请设置定时任务的时间</string>
<string name="invalid_proxy_host">代理服务器主机名解析失败proxyHost=%s</string>
<string name="bluetooth_state_changed">蓝牙状态变化</string>
<string name="specified_state">指定状态</string>
<string name="state_on">已打开</string>
<string name="state_off">已关闭</string>
<string name="bluetooth_discovery_finished">蓝牙设备搜索完成</string>
<string name="specified_result">指定结果</string>
<string name="discovered">已发现</string>
<string name="undiscovered">未发现</string>
<string name="bluetooth_acl_connected">蓝牙设备已连接</string>
<string name="bluetooth_acl_disconnected">蓝牙设备已断开</string>
<string name="specified_device">指定设备</string>
<string name="device_address_hint">蓝牙设备MAC地址</string>
<string name="bluetooth_not_supported">不支持蓝牙设备</string>
<string name="start_discovery">搜索设备</string>
<string name="invalid_bluetooth_mac_address">蓝牙设备MAC地址无效例如AA:BB:CC:DD:EE:FF</string>
</resources>

@ -1064,8 +1064,8 @@
<string name="ip_hint">可選內網廣播地址例如192.168.1.255</string>
<string name="ip_error">IP地址格式錯誤例如192.168.168.168</string>
<string name="ip_regex" tools:ignore="TypographyDashes">^((\\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])$</string>
<string name="mac_hint">必填,網卡mac例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">網卡mac格式錯誤例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_hint">必填例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">格式錯誤例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_regex" tools:ignore="TypographyDashes">^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$</string>
<string name="broadcast_address">內網廣播地址</string>
<string name="ip">IP</string>
@ -1156,6 +1156,12 @@
<string name="meter"></string>
<string name="uid">UID</string>
<string name="enable_bluetooth">啟用藍牙發現</string>
<string name="enable_bluetooth_dialog">必須啟用藍牙裝置發現服務才能繼續獲取!\n現在啟用嗎</string>
<string name="enable_bluetooth_tips">以支援自動任務等需要藍牙發現功能的功能</string>
<string name="scan_interval">掃描間隔</string>
<string name="ignore_anonymous">忽略匿名裝置</string>
<string name="task_name_status">任務名稱/狀態</string>
<string name="task_name">任務名稱</string>
<string name="task_description">任務描述</string>
@ -1401,10 +1407,26 @@
<string name="alarm_volume">播放音量</string>
<string name="alarm_play_times">播放次數(0=無限)</string>
<string name="invalid_tag">%s 標籤無效:%s</string>
<string name="invalid_tag" formatted="false">%s 標籤無效:%s</string>
<string name="invalid_task_name">請輸入任務名稱</string>
<string name="invalid_conditions">請添加觸發條件</string>
<string name="invalid_actions">請添加執行動作</string>
<string name="invalid_cron">請設置定時任務的時間</string>
<string name="invalid_proxy_host">代理伺服器主機名解析失敗proxyHost=%s</string>
<string name="bluetooth_state_changed">藍牙狀態變化</string>
<string name="specified_state">指定狀態</string>
<string name="state_on">已開啟</string>
<string name="state_off">已關閉</string>
<string name="bluetooth_discovery_finished">藍牙裝置搜索完成</string>
<string name="specified_result">指定結果</string>
<string name="discovered">已發現</string>
<string name="undiscovered">未發現</string>
<string name="bluetooth_acl_connected">藍牙裝置已連接</string>
<string name="bluetooth_acl_disconnected">藍牙裝置已斷開</string>
<string name="specified_device">指定裝置</string>
<string name="device_address_hint">藍牙裝置MAC地址</string>
<string name="bluetooth_not_supported">不支援藍牙裝置</string>
<string name="start_discovery">搜索裝置</string>
<string name="invalid_bluetooth_mac_address">藍牙裝置MAC地址無效例如AA:BB:CC:DD:EE:FF</string>
</resources>

@ -1064,8 +1064,8 @@
<string name="ip_hint">可选内网广播地址例如192.168.1.255</string>
<string name="ip_error">IP地址格式错误例如192.168.168.168</string>
<string name="ip_regex" tools:ignore="TypographyDashes">^((\\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])$</string>
<string name="mac_hint">必填,网卡mac例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">网卡mac格式错误例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_hint">必填例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_error">格式错误例如AA:BB:CC:DD:EE:FF</string>
<string name="mac_regex" tools:ignore="TypographyDashes">^((([a-fA-F0-9]{2}:){5})|(([a-fA-F0-9]{2}-){5}))[a-fA-F0-9]{2}$</string>
<string name="broadcast_address">内网广播地址</string>
<string name="ip">IP</string>
@ -1156,6 +1156,12 @@
<string name="meter"></string>
<string name="uid">UID</string>
<string name="enable_bluetooth">发现蓝牙设备服务</string>
<string name="enable_bluetooth_dialog">必须开启发现蓝牙设备服务,才能使用获取!\n是否立即启用</string>
<string name="enable_bluetooth_tips">以便支持 自动任务 等需要发现蓝牙的功能</string>
<string name="scan_interval">扫描间隔</string>
<string name="ignore_anonymous">忽略匿名设备</string>
<string name="task_name_status">任务名称/状态</string>
<string name="task_name">任务名称</string>
<string name="task_description">任务描述</string>
@ -1401,10 +1407,26 @@
<string name="alarm_volume">播放音量</string>
<string name="alarm_play_times">播放次数(0=无限)</string>
<string name="invalid_tag">%s 标签无效:%s</string>
<string name="invalid_tag" formatted="false">%s 标签无效:%s</string>
<string name="invalid_task_name">请输入任务名称</string>
<string name="invalid_conditions">请添加触发条件</string>
<string name="invalid_actions">请添加执行动作</string>
<string name="invalid_cron">请设置定时任务的时间</string>
<string name="invalid_proxy_host">代理服务器主机名解析失败proxyHost=%s</string>
<string name="bluetooth_state_changed">蓝牙状态变化</string>
<string name="specified_state">指定状态</string>
<string name="state_on">已打开</string>
<string name="state_off">已关闭</string>
<string name="bluetooth_discovery_finished">蓝牙设备搜索完成</string>
<string name="specified_result">指定结果</string>
<string name="discovered">已发现</string>
<string name="undiscovered">未发现</string>
<string name="bluetooth_acl_connected">蓝牙设备已连接</string>
<string name="bluetooth_acl_disconnected">蓝牙设备已断开</string>
<string name="specified_device">指定设备</string>
<string name="device_address_hint">蓝牙设备MAC地址</string>
<string name="bluetooth_not_supported">不支持蓝牙设备</string>
<string name="start_discovery">搜索设备</string>
<string name="invalid_bluetooth_mac_address">蓝牙设备MAC地址无效例如AA:BB:CC:DD:EE:FF</string>
</resources>

@ -170,6 +170,7 @@
<item name="android:layout_marginTop">5dp</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:minHeight">30dp</item>
<item name="android:singleLine">true</item>
<!--<item name="android:textSize">@dimen/text_size_small</item>-->
<item name="android:button">@drawable/custom_radio_button</item>

Loading…
Cancel
Save