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