新增:自动任务·快捷指令 —— 执行动作:播放警报 #385

pull/408/head
pppscn 5 months ago
parent b6c98e7f33
commit 9aeca6f3f6

@ -13,6 +13,7 @@ import android.os.Build
import androidx.lifecycle.MutableLiveData
import androidx.multidex.MultiDex
import androidx.work.Configuration
import androidx.work.WorkManager
import com.gyf.cactus.Cactus
import com.gyf.cactus.callback.CactusCallback
import com.gyf.cactus.ext.cactus
@ -137,6 +138,9 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
//纯客户端模式
if (SettingUtils.enablePureClientMode) return
//初始化WorkManager
WorkManager.initialize(this, Configuration.Builder().build())
//动态加载FrpcLib
val libPath = filesDir.absolutePath + "/libs"
val soFile = File(libPath)

@ -0,0 +1,11 @@
package com.idormy.sms.forwarder.entity.action
import java.io.Serializable
data class AlarmSetting(
var description: String = "", //描述
var action: String = "stop", //动作: start=启动警报, stop=停止警报
var volume: Int = 100, //播放音量
var loopTimes: Int = 5, //循环次数0=无限循环
var music: String = "", //音乐文件
) : Serializable

@ -194,6 +194,13 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
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
),
)
override fun initArgs() {

@ -0,0 +1,288 @@
package com.idormy.sms.forwarder.fragment.action
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
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.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentTasksActionAlarmBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.TaskSetting
import com.idormy.sms.forwarder.entity.action.AlarmSetting
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_ACTION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_ACTION
import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_ACTION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.TASK_ACTION_ALARM
import com.idormy.sms.forwarder.utils.TaskWorker
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.workers.ActionWorker
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.MaterialDialog
import java.io.File
import java.util.Date
@Page(name = "Alarm")
@Suppress("PrivatePropertyName", "DEPRECATION")
class AlarmFragment : BaseFragment<FragmentTasksActionAlarmBinding?>(), View.OnClickListener {
private val TAG: String = AlarmFragment::class.java.simpleName
private var titleBar: TitleBar? = null
private var mCountDownHelper: CountDownButtonHelper? = null
private var appContext: App? = null
@JvmField
@AutoWired(name = KEY_EVENT_DATA_ACTION)
var eventData: String? = null
override fun initArgs() {
XRouter.getInstance().inject(this)
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentTasksActionAlarmBinding {
return FragmentTasksActionAlarmBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.task_alarm)
return titleBar
}
/**
* 初始化控件
*/
override fun initViews() {
appContext = requireActivity().application as App
//测试按钮增加倒计时,避免重复点击
mCountDownHelper = CountDownButtonHelper(binding!!.btnTest, 2)
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
override fun onCountDown(time: Int) {
binding!!.btnTest.text = String.format(getString(R.string.seconds_n), time)
}
override fun onFinished() {
binding!!.btnTest.text = getString(R.string.test)
}
})
Log.d(TAG, "initViews eventData:$eventData")
if (eventData != null) {
val settingVo = Gson().fromJson(eventData, AlarmSetting::class.java)
Log.d(TAG, "initViews settingVo:$settingVo")
if (settingVo.action == "start") {
binding!!.rgAlarmState.check(R.id.rb_start_alarm)
binding!!.layoutAlarmSettings.visibility = View.VISIBLE
} else {
binding!!.rgAlarmState.check(R.id.rb_stop_alarm)
binding!!.layoutAlarmSettings.visibility = View.GONE
}
binding!!.xsbVolume.setDefaultValue(settingVo.volume)
binding!!.xsbLoopTimes.setDefaultValue(settingVo.loopTimes)
binding!!.etMusicPath.setText(settingVo.music)
} else {
binding!!.xsbVolume.setDefaultValue(100)
binding!!.xsbLoopTimes.setDefaultValue(5)
}
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
binding!!.btnFilePicker.setOnClickListener(this)
binding!!.xsbVolume.setOnSeekBarListener { _, _ ->
checkSetting(true)
}
binding!!.xsbLoopTimes.setOnSeekBarListener { _, _ ->
checkSetting(true)
}
binding!!.rgAlarmState.setOnCheckedChangeListener { _, checkedId ->
binding!!.layoutAlarmSettings.visibility = if (checkedId == R.id.rb_start_alarm) View.VISIBLE else View.GONE
checkSetting(true)
}
}
@SingleClick
override fun onClick(v: View) {
try {
when (v.id) {
R.id.btn_file_picker -> {
// 申请储存权限
XXPermissions.with(this).permission(Permission.MANAGE_EXTERNAL_STORAGE).request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List<String>, all: Boolean) {
val downloadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
val fileList = findAudioFiles(downloadPath)
if (fileList.isEmpty()) {
XToastUtils.error(String.format(getString(R.string.download_music_first), downloadPath))
return
}
MaterialDialog.Builder(requireContext()).title(getString(R.string.alarm_music)).content(String.format(getString(R.string.root_directory), downloadPath)).items(fileList).itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
val webPath = "$downloadPath/$text"
binding!!.etMusicPath.setText(webPath)
checkSetting(true)
true // allow selection
}.positiveText(R.string.select).negativeText(R.string.cancel).show()
}
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!!.etMusicPath.setText(getString(R.string.storage_permission_tips))
}
})
}
R.id.btn_test -> {
// 申请修改系统设置权限
XXPermissions.with(this).permission(Permission.WRITE_SETTINGS).request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List<String>, all: Boolean) {
mCountDownHelper?.start()
try {
val settingVo = checkSetting()
Log.d(TAG, settingVo.toString())
val taskAction = TaskSetting(TASK_ACTION_ALARM, getString(R.string.task_alarm), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_alarm), settingVo.description, Date(), getString(R.string.task_alarm))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
mCountDownHelper?.finish()
e.printStackTrace()
Log.e(TAG, "onClick error: ${e.message}")
XToastUtils.error(e.message.toString(), 30000)
}
}
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!!.tvDescription.text = getString(R.string.write_settings_permission_tips)
}
})
return
}
R.id.btn_del -> {
popToBack()
return
}
R.id.btn_save -> {
// 申请修改系统设置权限
XXPermissions.with(this).permission(Permission.WRITE_SETTINGS).request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List<String>, all: Boolean) {
val settingVo = checkSetting()
val intent = Intent()
intent.putExtra(KEY_BACK_DESCRIPTION_ACTION, settingVo.description)
intent.putExtra(KEY_BACK_DATA_ACTION, Gson().toJson(settingVo))
setFragmentResult(TASK_ACTION_ALARM, intent)
popToBack()
}
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!!.tvDescription.text = getString(R.string.write_settings_permission_tips)
}
})
return
}
}
} catch (e: Exception) {
XToastUtils.error(e.message.toString(), 30000)
e.printStackTrace()
Log.e(TAG, "onClick error: ${e.message}")
}
}
//检查设置
@SuppressLint("SetTextI18n")
private fun checkSetting(updateView: Boolean = false): AlarmSetting {
val volume = binding!!.xsbVolume.selectedNumber
val loopTimes = binding!!.xsbLoopTimes.selectedNumber
val music = binding!!.etMusicPath.text.toString().trim()
val description = StringBuilder()
val action = if (binding!!.rgAlarmState.checkedRadioButtonId == R.id.rb_start_alarm) {
description.append(getString(R.string.start_alarm))
description.append(", ").append(getString(R.string.alarm_volume)).append(":").append(volume).append("%")
description.append(", ").append(getString(R.string.alarm_loop_times)).append(":").append(loopTimes)
if (music.isNotEmpty()) {
description.append(", ").append(getString(R.string.alarm_music)).append(":").append(music)
}
"start"
} else {
description.append(getString(R.string.stop_alarm))
"stop"
}
if (updateView) {
binding!!.tvDescription.text = description.toString()
}
return AlarmSetting(description.toString(), action, volume, loopTimes, music)
}
private fun findAudioFiles(directoryPath: String): List<String> {
val audioFiles = mutableListOf<String>()
val directory = File(directoryPath)
if (directory.exists() && directory.isDirectory) {
directory.listFiles()?.let { files ->
// 筛选出支持的音频文件
files.filter { it.isFile && isSupportedAudioFile(it) }.forEach { audioFiles.add(it.name) }
}
}
return audioFiles
}
private fun isSupportedAudioFile(file: File): Boolean {
val supportedExtensions = listOf("mp3", "ogg", "wav")
return supportedExtensions.any { it.equals(file.extension, ignoreCase = true) }
}
}

@ -110,7 +110,7 @@ class FrpcFragment : BaseFragment<FragmentTasksActionFrpcBinding?>(), View.OnCli
Log.d(TAG, "initViews settingVo:$settingVo")
}
//初始化发送通道下拉框
//初始化Frpc下拉框
initFrpc()
}

@ -5,6 +5,9 @@ import android.app.*
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
import android.os.IBinder
import android.text.TextUtils
@ -16,6 +19,7 @@ import com.idormy.sms.forwarder.App
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.task.CronJobScheduler
import com.idormy.sms.forwarder.workers.LoadAppListWorker
@ -31,6 +35,7 @@ import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import java.io.File
@SuppressLint("SimpleDateFormat")
@Suppress("PrivatePropertyName", "DeferredResultUnused", "OPT_IN_USAGE", "DEPRECATION")
@ -68,6 +73,79 @@ class ForegroundService : Service() {
})
}
private var alarmPlayer: MediaPlayer? = null
private var alarmLoopCount = 0
private val alarmObserver = Observer<AlarmSetting> { alarm ->
Log.d(TAG, "Received alarm: $alarm")
alarmPlayer?.release()
alarmPlayer = null
if (alarm.action == "start") {
//获取音量
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
Log.d(TAG, "maxVolume=$maxVolume, currentVolume=$currentVolume")
//设置音量
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, (maxVolume * alarm.volume / 100), 0)
//播放音乐
alarmPlayer = MediaPlayer().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val audioAttributes = AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build()
setAudioAttributes(audioAttributes)
} else {
// 对于 Android 5.0 之前的版本,使用 setAudioStreamType
val audioStreamType = AudioManager.STREAM_ALARM
setAudioStreamType(audioStreamType)
}
try {
if (alarm.music.isEmpty() || !File(alarm.music).exists()) {
val fd = resources.openRawResourceFd(R.raw.alarm)
setDataSource(fd.fileDescriptor, fd.startOffset, fd.length)
} else {
setDataSource(alarm.music)
}
setOnPreparedListener {
Log.d(TAG, "MediaPlayer prepared")
start()
//更新通知栏
updateNotification(alarm.description, R.drawable.auto_task_icon_alarm, true)
}
setOnCompletionListener {
Log.d(TAG, "MediaPlayer completed")
if (alarm.loopTimes == 0 || alarmLoopCount < alarm.loopTimes) {
start()
alarmLoopCount++
} else {
stop()
reset()
release()
alarmPlayer = null
alarmLoopCount = 0
//恢复音量
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, currentVolume, 0)
//恢复通知栏
updateNotification(SettingUtils.notifyContent)
}
}
setOnErrorListener { _, what, extra ->
Log.e(TAG, "MediaPlayer error: what=$what, extra=$extra")
release()
return@setOnErrorListener true
}
setVolume(alarm.volume / 100F, alarm.volume / 100F)
prepareAsync()
} catch (e: Exception) {
Log.e(TAG, "MediaPlayer Exception: ${e.message}")
}
}
}
}
companion object {
var isRunning = false
}
@ -100,6 +178,12 @@ class ForegroundService : Service() {
val updatedContent = intent.getStringExtra("UPDATED_CONTENT")
updateNotification(updatedContent ?: "")
}
"STOP_ALARM" -> {
alarmPlayer?.release()
alarmPlayer = null
updateNotification(SettingUtils.notifyContent)
}
}
}
return START_STICKY
@ -148,7 +232,7 @@ class ForegroundService : Service() {
//启动 Frpc
if (App.FrpclibInited) {
//监听Frpc启动指令
LiveEventBus.get(INTENT_FRPC_APPLY_FILE, String::class.java).observeStickyForever(frpcObserver)
LiveEventBus.get(INTENT_FRPC_APPLY_FILE, String::class.java).observeForever(frpcObserver)
//自启动的Frpc
GlobalScope.async(Dispatchers.IO) {
val frpcList = Core.frpc.getAutorun()
@ -166,6 +250,10 @@ class ForegroundService : Service() {
}
}
}
//播放警报
LiveEventBus.get<AlarmSetting>(EVENT_ALARM_ACTION).observeForever(alarmObserver)
} catch (e: Exception) {
handleException(e, "startForegroundService")
}
@ -178,6 +266,8 @@ class ForegroundService : Service() {
stopSelf()
compositeDisposable.dispose()
isRunning = false
alarmPlayer?.release()
alarmPlayer = null
} catch (e: Exception) {
handleException(e, "stopForegroundService")
}
@ -188,7 +278,7 @@ class ForegroundService : Service() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_HIGH
val notificationChannel = NotificationChannel(FRONT_CHANNEL_ID, FRONT_CHANNEL_NAME, importance)
notificationChannel.description = "Frpc Foreground Service"
notificationChannel.description = getString(R.string.notification_content)
notificationChannel.enableLights(true)
notificationChannel.lightColor = Color.GREEN
notificationChannel.vibrationPattern = longArrayOf(0, 1000, 500, 1000)
@ -199,17 +289,35 @@ class ForegroundService : Service() {
}
}
private fun createNotification(content: String): Notification {
private fun createNotification(content: String, largeIconResId: Int? = null, showStopButton: Boolean = false): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val flags = if (Build.VERSION.SDK_INT >= 30) PendingIntent.FLAG_IMMUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
val pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags)
return NotificationCompat.Builder(this, FRONT_CHANNEL_ID).setContentTitle(getString(R.string.app_name)).setContentText(content).setSmallIcon(R.drawable.ic_forwarder).setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_menu_frpc)).setContentIntent(pendingIntent).setWhen(System.currentTimeMillis()).build()
val builder = NotificationCompat.Builder(this, FRONT_CHANNEL_ID).setContentTitle(getString(R.string.app_name)).setContentText(content).setSmallIcon(R.drawable.ic_forwarder).setContentIntent(pendingIntent).setWhen(System.currentTimeMillis())
// 设置大图标(可选)
if (largeIconResId != null) {
builder.setLargeIcon(BitmapFactory.decodeResource(resources, largeIconResId))
} else {
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.ic_menu_frpc))
}
// 添加停止按钮(可选)
if (showStopButton) {
val stopIntent = Intent(this, ForegroundService::class.java).apply {
action = "STOP_ALARM"
}
val stopPendingIntent = PendingIntent.getService(this, 0, stopIntent, flags)
builder.addAction(R.drawable.ic_stop, getString(R.string.stop), stopPendingIntent)
}
return builder.build()
}
private fun updateNotification(updatedContent: String) {
private fun updateNotification(updatedContent: String, largeIconResId: Int? = null, showStopButton: Boolean = false) {
try {
val notification = createNotification(updatedContent)
val notification = createNotification(updatedContent, largeIconResId, showStopButton)
notificationManager?.notify(FRONT_NOTIFY_ID, notification)
} catch (e: Exception) {
handleException(e, "updateNotification")

@ -77,7 +77,6 @@ const val SP_SMS_TEMPLATE = "sms_template"
const val SP_PURE_CLIENT_MODE = "enable_pure_client_mode"
const val SP_PURE_TASK_MODE = "enable_pure_task_mode"
const val SP_DEBUG_MODE = "enable_debug_mode"
const val SP_ACCESSIBILITY_SERVICE = "enable_accessibility_service"
const val SP_LOCATION = "enable_location"
const val SP_LOCATION_ACCURACY = "location_accuracy"
const val SP_LOCATION_POWER_REQUIREMENT = "location_power_requirement"
@ -151,6 +150,9 @@ const val EVENT_FRPC_RUNNING_SUCCESS = "EVENT_FRPC_RUNNING_SUCCESS"
const val INTENT_FRPC_EDIT_FILE = "INTENT_FRPC_EDIT_FILE"
const val INTENT_FRPC_APPLY_FILE = "INTENT_FRPC_APPLY_FILE"
//声音警报
const val EVENT_ALARM_ACTION = "EVENT_ALARM_ACTION"
//吐司监听
const val EVENT_TOAST_SUCCESS = "key_toast_success"
const val EVENT_TOAST_ERROR = "key_toast_error"
@ -241,6 +243,7 @@ const val TASK_ACTION_FRPC = 2004
const val TASK_ACTION_HTTPSERVER = 2005
const val TASK_ACTION_RULE = 2006
const val TASK_ACTION_SENDER = 2007
const val TASK_ACTION_ALARM = 2008
const val SP_BATTERY_INFO = "battery_info"
const val SP_BATTERY_STATUS = "battery_status"

@ -16,6 +16,7 @@ import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.TaskSetting
import com.idormy.sms.forwarder.entity.action.AlarmSetting
import com.idormy.sms.forwarder.entity.action.CleanerSetting
import com.idormy.sms.forwarder.entity.action.FrpcSetting
import com.idormy.sms.forwarder.entity.action.HttpServerSetting
@ -26,6 +27,7 @@ import com.idormy.sms.forwarder.entity.action.SmsSetting
import com.idormy.sms.forwarder.service.HttpServerService
import com.idormy.sms.forwarder.service.LocationService
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
import com.idormy.sms.forwarder.utils.EVENT_TOAST_INFO
import com.idormy.sms.forwarder.utils.EVENT_TOAST_SUCCESS
@ -36,6 +38,7 @@ import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.PhoneUtils
import com.idormy.sms.forwarder.utils.SendUtils
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.TASK_ACTION_ALARM
import com.idormy.sms.forwarder.utils.TASK_ACTION_CLEANER
import com.idormy.sms.forwarder.utils.TASK_ACTION_FRPC
import com.idormy.sms.forwarder.utils.TASK_ACTION_HTTPSERVER
@ -305,6 +308,20 @@ class ActionWorker(context: Context, params: WorkerParameters) : CoroutineWorker
writeLog(String.format(getString(R.string.successful_execution), senderSetting.description), "SUCCESS")
}
TASK_ACTION_ALARM -> {
val alarmSetting = Gson().fromJson(action.setting, AlarmSetting::class.java)
if (alarmSetting == null) {
writeLog("alarmSetting is null")
continue
}
// 发送开始播放指令
LiveEventBus.get<AlarmSetting>(EVENT_ALARM_ACTION).post(alarmSetting)
successNum++
writeLog(String.format(getString(R.string.successful_execution), alarmSetting.description), "SUCCESS")
}
else -> {
writeLog("action.type is ${action.type}")
}

@ -0,0 +1,20 @@
<?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="#ffff8514"
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" />
<group
android:scaleX="0.014"
android:scaleY="0.014"
android:translateX="3.7"
android:translateY="5.5">
<path
android:fillColor="#ffffff"
android:pathData="M704,129.9a289.9,289.9 0,0 1,256 287.9v262.2a64,64 0,0 0,20.1 46.6l23.8,22.4a64,64 0,0 1,20.1 46.6L1024,832a64,64 0,0 1,-64 64h-160c-21.3,85.3 -74.7,128 -160,128s-138.7,-42.7 -160,-128L320,896a64,64 0,0 1,-64 -64v-37.5a64,64 0,0 1,18.8 -45.2l26.5,-26.5A64,64 0,0 0,320 677.5L320,416a288,288 0,0 1,256 -286.3L576,64a64,64 0,1 1,128 0v65.9zM270.5,141.6a32,32 0,0 1,35.1 53.6c-27,17.7 -48.1,44.5 -63.6,81.5 -15.6,37.4 -21.2,85.8 -16.2,145a32,32 0,0 1,-63.8 5.4c-5.8,-68.8 1,-127.2 20.9,-175 20.1,-48.3 49.3,-85.4 87.6,-110.5zM1009.5,141.6c38.2,25 67.5,62.1 87.6,110.5 20,47.9 26.7,106.2 20.9,175a32,32 0,0 1,-63.8 -5.4c5,-59.3 -0.6,-107.5 -16.2,-145 -15.4,-37 -36.5,-63.9 -63.6,-81.5a32,32 0,1 1,35.1 -53.6zM137.3,17.8a32,32 0,1 1,45.3 45.2c-39,39 -68.5,88.1 -88.3,147.5C74.2,271 64,342.4 64,424.4a32,32 0,0 1,-64 0c0,-88.6 11.1,-166.7 33.7,-234.1C56.5,121.7 91.1,64 137.3,17.8zM1142.7,17.8c46.3,46.3 80.8,103.9 103.7,172.5 22.5,67.5 33.7,145.5 33.7,234.1a32,32 0,1 1,-64 0c0,-82.1 -10.2,-153.4 -30.3,-213.9 -19.8,-59.5 -49.3,-108.5 -88.3,-147.5a32,32 0,0 1,45.3 -45.2z" />
</group>
</vector>

@ -0,0 +1,20 @@
<?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" />
<group
android:scaleX="0.014"
android:scaleY="0.014"
android:translateX="3.7"
android:translateY="5.5">
<path
android:fillColor="#ffffffff"
android:pathData="M704,129.9a289.9,289.9 0,0 1,256 287.9v262.2a64,64 0,0 0,20.1 46.6l23.8,22.4a64,64 0,0 1,20.1 46.6L1024,832a64,64 0,0 1,-64 64h-160c-21.3,85.3 -74.7,128 -160,128s-138.7,-42.7 -160,-128L320,896a64,64 0,0 1,-64 -64v-37.5a64,64 0,0 1,18.8 -45.2l26.5,-26.5A64,64 0,0 0,320 677.5L320,416a288,288 0,0 1,256 -286.3L576,64a64,64 0,1 1,128 0v65.9zM270.5,141.6a32,32 0,0 1,35.1 53.6c-27,17.7 -48.1,44.5 -63.6,81.5 -15.6,37.4 -21.2,85.8 -16.2,145a32,32 0,0 1,-63.8 5.4c-5.8,-68.8 1,-127.2 20.9,-175 20.1,-48.3 49.3,-85.4 87.6,-110.5zM1009.5,141.6c38.2,25 67.5,62.1 87.6,110.5 20,47.9 26.7,106.2 20.9,175a32,32 0,0 1,-63.8 -5.4c5,-59.3 -0.6,-107.5 -16.2,-145 -15.4,-37 -36.5,-63.9 -63.6,-81.5a32,32 0,1 1,35.1 -53.6zM137.3,17.8a32,32 0,1 1,45.3 45.2c-39,39 -68.5,88.1 -88.3,147.5C74.2,271 64,342.4 64,424.4a32,32 0,0 1,-64 0c0,-88.6 11.1,-166.7 33.7,-234.1C56.5,121.7 91.1,64 137.3,17.8zM1142.7,17.8c46.3,46.3 80.8,103.9 103.7,172.5 22.5,67.5 33.7,145.5 33.7,234.1a32,32 0,1 1,-64 0c0,-82.1 -10.2,-153.4 -30.3,-213.9 -19.8,-59.5 -49.3,-108.5 -88.3,-147.5a32,32 0,0 1,45.3 -45.2z" />
</group>
</vector>

@ -0,0 +1,223 @@
<?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_alarm"
app:srcCompat="@drawable/auto_task_icon_alarm"
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_alarm"
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_alarm_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_alarm_state"
style="@style/rg_style"
android:orientation="horizontal"
android:paddingBottom="@dimen/config_padding_5dp">
<RadioButton
android:id="@+id/rb_start_alarm"
style="@style/rg_rb_style"
android:checked="true"
android:text="@string/start_alarm"
tools:ignore="TouchTargetSizeCheck" />
<RadioButton
android:id="@+id/rb_stop_alarm"
style="@style/rg_rb_style"
android:text="@string/stop_alarm"
tools:ignore="TouchTargetSizeCheck" />
</RadioGroup>
</LinearLayout>
<LinearLayout
android:id="@+id/layout_alarm_settings"
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/alarm_play_settings"
android:textStyle="bold"
tools:ignore="RelativeOverlap" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="5dp"
android:background="?attr/xui_config_color_separator_light" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_volume"
android:textStyle="bold" />
<com.xuexiang.xui.widget.picker.XSeekBar
android:id="@+id/xsb_volume"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:padding="0dp"
app:xsb_max="100"
app:xsb_min="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_loop_times"
android:textStyle="bold" />
<com.xuexiang.xui.widget.picker.XSeekBar
android:id="@+id/xsb_loop_times"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:padding="0dp"
app:xsb_max="30"
app:xsb_min="0" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alarm_music"
android:textStyle="bold" />
<com.xuexiang.xui.widget.edittext.ClearEditText
android:id="@+id/et_music_path"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_weight="1"
android:hint="@string/alarm_music_tips"
android:textSize="@dimen/text_size_mini"
tools:ignore="SmallSp" />
<com.xuexiang.xui.widget.button.shadowbutton.RippleShadowShadowButton
android:id="@+id/btn_file_picker"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:gravity="center"
android:padding="5dp"
android:text="@string/select_file"
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>
</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" />
<com.xuexiang.xui.widget.textview.supertextview.SuperButton
android:id="@+id/btn_test"
style="@style/SuperButton.Green.Icon.Spacing"
android:drawableStart="@drawable/ic_test"
android:text="@string/test"
tools:ignore="RtlSymmetry,TextContrastCheck,TouchTargetSizeCheck" />
</LinearLayout>
</LinearLayout>

Binary file not shown.

@ -210,7 +210,7 @@
<string name="delete_log_toast">The log entry is deleted.</string>
<string name="delete_type_log_tips">Are you sure you want to delete all log records for this category?</string>
<string name="delete_type_log_toast">The category log record has been cleared!</string>
<string name="resend_toast">Attempting to resend over the original sending channel</string>
<string name="resend_toast">Attempting to resend over the original sender</string>
<string name="rematch_toast">Rematching rule sending</string>
<string name="details">Details</string>
<string name="clear_logs_tips">Are you sure to clear all forwarding logs?</string>
@ -436,8 +436,8 @@
<string name="ssl">SSL</string>
<!--CloneActivity-->
<string name="local_ip">Local IP: </string>
<string name="operating_instruction">Important Note:\nThis feature is intended solely for personal use in switching between old and new phones. Any consequences arising from illegal use are the user\'s responsibility!\n\nInstructions:\n1. Connect both old and new phones to the same WiFi network (disable AP isolation). If internal network penetration is needed, configure Frpc first.\n2. [Choose One] On the old phone, tap the "Push" button to send this device\'s configuration to the server.\n3. [Choose One] On the new phone, tap the "Pull" button to fetch the server\'s configuration to this device.\n\nNotes:\n1. The client and server app versions must match for successful cloning.\n2. Upon successful import, the sending channels and forwarding rules will be entirely replaced, clearing the historical records.\n3. Active requests, keep-alive measures, and personal settings are not included in the cloning scope.\n4. After successful import, it\'s crucial to re-enter the [General Settings] and toggle on the functions you need! (Or manually grant permissions in system settings).</string>
<string name="operating_instruction_offline">Important Note:\nThis feature is strictly intended for personal use in switching between old and new phones. Any consequences arising from illegal use are the user\'s responsibility!\n\nNotes:\n1. The exporting and importing apps must have identical versions for one-click cloning to work!\n2. Upon successful import on the new phone, the sending channels and forwarding rules will be entirely replaced, clearing the history records!\n3. Active requests, keep-alive measures, and personal settings are not included in the cloning process.\n4. After a successful import, it\'s crucial to re-enter the [General Settings] and toggle on the functions you need! (Or manually grant permissions in system settings).</string>
<string name="operating_instruction">Important Note:\nThis feature is intended solely for personal use in switching between old and new phones. Any consequences arising from illegal use are the user\'s responsibility!\n\nInstructions:\n1. Connect both old and new phones to the same WiFi network (disable AP isolation). If internal network penetration is needed, configure Frpc first.\n2. [Choose One] On the old phone, tap the "Push" button to send this device\'s configuration to the server.\n3. [Choose One] On the new phone, tap the "Pull" button to fetch the server\'s configuration to this device.\n\nNotes:\n1. The client and server app versions must match for successful cloning.\n2. Upon successful import, the senders and forwarding rules will be entirely replaced, clearing the historical records.\n3. Active requests, keep-alive measures, and personal settings are not included in the cloning scope.\n4. After successful import, it\'s crucial to re-enter the [General Settings] and toggle on the functions you need! (Or manually grant permissions in system settings).</string>
<string name="operating_instruction_offline">Important Note:\nThis feature is strictly intended for personal use in switching between old and new phones. Any consequences arising from illegal use are the user\'s responsibility!\n\nNotes:\n1. The exporting and importing apps must have identical versions for one-click cloning to work!\n2. Upon successful import on the new phone, the senders and forwarding rules will be entirely replaced, clearing the history records!\n3. Active requests, keep-alive measures, and personal settings are not included in the cloning process.\n4. After a successful import, it\'s crucial to re-enter the [General Settings] and toggle on the functions you need! (Or manually grant permissions in system settings).</string>
<string name="push">Push</string>
<string name="pull">Pull</string>
<string name="stop">Stop</string>
@ -589,7 +589,7 @@
<string name="seconds_n">%s sec</string>
<string name="retry">Retry</string>
<string name="retry_label">Max Retries</string>
<string name="test_sender_sms">[%s] Congratulations, the sending channel test is successful, please continue to add forwarding rules!</string>
<string name="test_sender_sms">[%s] Congratulations, the sender test is successful, please continue to add forwarding rules!</string>
<string name="test_sender_name">Test Channel</string>
<string name="test_sim_info" tools:ignore="Typos">SIM1_TestOperator_18888888888</string>
<string name="keep_reminding">Keep Reminding</string>
@ -822,7 +822,7 @@
<string name="header_value">Value</string>
<string name="header_add">Add header</string>
<string name="header_del">Del header</string>
<string name="select_sender_type">Please select send channel type</string>
<string name="select_sender_type">Please select sender type</string>
<string name="feishu_webhook_hint">Group Robot → Webhook Address</string>
<string name="feishu_secret_hint">Group Robot → Security Settings → Signature Verification</string>
<string name="pushplus_token_hint">Please go to the corresponding official website to obtain</string>
@ -976,15 +976,16 @@
<string name="disabled_on_the_server">Disable this feature on the server</string>
<string name="frpc_failed_to_run">Frpc failed to run</string>
<string name="successfully_deleted">Successfully deleted</string>
<string name="sender_disabled_tips">[Note] The sending channel has been disabled, and its associated rules will not be sent even if they match!</string>
<string name="sender_disabled_tips">[Note] The sender has been disabled, and its associated rules will not be sent even if they match!</string>
<string name="rule_disabled_tips">[Note] The rule has been disabled, will not be sent even if they match!</string>
<string name="sender_contains_tips">[Note] The sending channel is already in the list, no need to add it again!</string>
<string name="sender_contains_tips">[Note] The sender is already in the list, no need to add it again!</string>
<string name="rule_contains_tips">[Note] The rule is already in the list, no need to add it again!</string>
<string name="frpc_contains_tips">[Note] The frpc is already in the list, no need to add it again!</string>
<string name="local_call">Local Call:</string>
<string name="remote_sms">Remote SMS</string>
<string name="clear">Clear</string>
<string name="storage_permission_tips">Unauthorized storage permission, this function cannot be used!</string>
<string name="write_settings_permission_tips">Unauthorized write settings permission, this function cannot be used!</string>
<string name="contact_info" formatted="false">Name%s\nPhone%s</string>
<string name="card_slot_does_not_match">Card slot does not match the rule</string>
<string name="unmatched_rule">Unmatched rule</string>
@ -1061,9 +1062,11 @@
<string name="wol_port_error">Port number value range: 1~65535</string>
<string name="wol_port_regex" tools:ignore="TypographyDashes">^([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$</string>
<string name="select_directory">Select Dir</string>
<string name="select_file">Select File</string>
<string name="web_client">Web Client</string>
<string name="restarting_httpserver">Restarting HttpServer</string>
<string name="download_first">Download and unzip to:\n%s</string>
<string name="download_music_first">Download music file to:\n%s</string>
<string name="root_directory">Root Directory:\n%s</string>
<string name="select_web_client_directory">Select WebClient Directory</string>
<string name="invalid_feishu_app_parameter">AppId/AppSecret/UserId cannot be empty</string>
@ -1193,10 +1196,11 @@
<string name="task_settings">Settings</string>
<string name="task_settings_tips">Control the configuration switch of "Settings".</string>
<string name="task_rule">Rules On/Off</string>
<string name="task_rule_tips">Control enabling/disabling of "Forwarding Rules"</string>
<string name="task_rule_tips">Control enabling/disabling of "Rules"</string>
<string name="task_sender">Channels On/Off</string>
<string name="task_sender_tips">Control enabling/disabling of "Sending Channels"</string>
<string name="task_sender_tips">Control enabling/disabling of "Senders"</string>
<string name="task_alarm">Alarm</string>
<string name="task_alarm_tips">Alarm</string>
<string name="second">Second</string>
<string name="minute">Minute</string>
@ -1353,4 +1357,12 @@
<string name="toast_location_not_enabled">Location is not enabled, Please go to system settings and activate it.</string>
<string name="task_condition_check_again">Recheck when delaying execution.</string>
<string name="task_condition_check_again_tips">When used as a triggering condition, recheck during delayed action execution.</string>
<string name="start_alarm">Start Alarm</string>
<string name="stop_alarm">Stop Alarm</string>
<string name="alarm_play_settings">Playback Settings</string>
<string name="alarm_music">Specify Music</string>
<string name="alarm_music_tips">Optional, download mp3/ogg/wav to the Download directory.</string>
<string name="alarm_volume">Alarm Volume</string>
<string name="alarm_loop_times">Loop Times(0=Infinite)</string>
</resources>

@ -986,6 +986,7 @@
<string name="remote_sms">远程发短信:</string>
<string name="clear">清除</string>
<string name="storage_permission_tips">未授权储存权限,该功能无法使用!</string>
<string name="write_settings_permission_tips">未授权修改系统设置权限,该功能无法使用!</string>
<string name="contact_info" formatted="false">姓名:%s\n号码%s</string>
<string name="card_slot_does_not_match">卡槽未匹配中规则</string>
<string name="unmatched_rule">未匹配中规则</string>
@ -1062,9 +1063,11 @@
<string name="wol_port_error">端口号取值范围1~65535</string>
<string name="wol_port_regex" tools:ignore="TypographyDashes">^([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$</string>
<string name="select_directory">选择目录</string>
<string name="select_file">选择文件</string>
<string name="web_client">Web客户端</string>
<string name="restarting_httpserver">正在重启HttpServer</string>
<string name="download_first">请先下载Web客户端并解压到\n%s</string>
<string name="download_music_first">请先下载音乐文件到:\n%s</string>
<string name="root_directory">根目录:\n%s</string>
<string name="select_web_client_directory">选择Web客户端目录</string>
<string name="invalid_feishu_app_parameter">AppId/AppSecret/UserId都不能为空</string>
@ -1198,6 +1201,7 @@
<string name="task_sender">启停通道</string>
<string name="task_sender_tips">控制【发送通道】的启用/禁用</string>
<string name="task_alarm">声音警报</string>
<string name="task_alarm_tips">声音警报</string>
<string name="second"></string>
<string name="minute"></string>
@ -1354,4 +1358,12 @@
<string name="toast_location_not_enabled">位置服务未开启,请先前往系统设置中开启!</string>
<string name="task_condition_check_again">延迟执行时再次校验</string>
<string name="task_condition_check_again_tips">作为触发条件时,在延迟执行动作时再次校验是否满足</string>
<string name="start_alarm">启动警报</string>
<string name="stop_alarm">停止警报</string>
<string name="alarm_play_settings">播放设置</string>
<string name="alarm_music">指定音乐</string>
<string name="alarm_music_tips">可选,下载 mp3/ogg/wav 到 Download 目录</string>
<string name="alarm_volume">播放音量</string>
<string name="alarm_loop_times">循环次数(0=无限)</string>
</resources>

@ -986,6 +986,7 @@
<string name="remote_sms">遠程發簡訊:</string>
<string name="clear">清除</string>
<string name="storage_permission_tips">未授權儲存權限,該功能無法使用!</string>
<string name="write_settings_permission_tips">未授權修改系統設置權限,該功能無法使用!</string>
<string name="contact_info" formatted="false">姓名:%s\n號碼%s</string>
<string name="card_slot_does_not_match">卡槽未匹配中規則</string>
<string name="unmatched_rule">未匹配中規則</string>
@ -1062,9 +1063,11 @@
<string name="wol_port_error">端口號取值範圍1~65535</string>
<string name="wol_port_regex" tools:ignore="TypographyDashes">^([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$</string>
<string name="select_directory">選擇目錄</string>
<string name="select_file">選擇文件</string>
<string name="web_client">Web客戶端</string>
<string name="restarting_httpserver">正在重啟HttpServer</string>
<string name="download_first">請先下載Web客戶端並解壓到\n%s</string>
<string name="download_music_first">請先下載音樂文件到:\n%s</string>
<string name="root_directory">根目錄:\n%s</string>
<string name="select_web_client_directory">選擇Web客戶端目錄</string>
<string name="invalid_feishu_app_parameter">AppId/AppSecret/UserId都不能為空</string>
@ -1198,6 +1201,7 @@
<string name="task_sender">啟停通道</string>
<string name="task_sender_tips">控制【發送通道】的啟用/禁用</string>
<string name="task_alarm">聲音警報</string>
<string name="task_alarm_tips">聲音警報</string>
<string name="second"></string>
<string name="minute"></string>
@ -1355,4 +1359,12 @@
<string name="toast_location_not_enabled">定位服務未開啟,請先前往系統設置中開啟!</string>
<string name="task_condition_check_again">延遲執行時再次校驗</string>
<string name="task_condition_check_again_tips">作為觸發條件時,在延遲執行動作時再次校驗是否滿足</string>
<string name="start_alarm">啟動警報</string>
<string name="stop_alarm">停止警報</string>
<string name="alarm_play_settings">播放設置</string>
<string name="alarm_music">指定音樂</string>
<string name="alarm_music_tips">可選,下載 mp3/ogg/wav 到 Download 目錄</string>
<string name="alarm_volume">播放音量</string>
<string name="alarm_loop_times">循環次數(0=無限)</string>
</resources>

@ -986,6 +986,7 @@
<string name="remote_sms">远程发短信:</string>
<string name="clear">清除</string>
<string name="storage_permission_tips">未授权储存权限,该功能无法使用!</string>
<string name="write_settings_permission_tips">未授权修改系统设置权限,该功能无法使用!</string>
<string name="contact_info" formatted="false">姓名:%s\n号码%s</string>
<string name="card_slot_does_not_match">卡槽未匹配中规则</string>
<string name="unmatched_rule">未匹配中规则</string>
@ -1062,9 +1063,11 @@
<string name="wol_port_error">端口号取值范围1~65535</string>
<string name="wol_port_regex" tools:ignore="TypographyDashes">^([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$</string>
<string name="select_directory">选择目录</string>
<string name="select_file">选择文件</string>
<string name="web_client">Web客户端</string>
<string name="restarting_httpserver">正在重启HttpServer</string>
<string name="download_first">请先下载Web客户端并解压到\n%s</string>
<string name="download_music_first">请先下载音乐文件到:\n%s</string>
<string name="root_directory">根目录:\n%s</string>
<string name="select_web_client_directory">选择Web客户端目录</string>
<string name="invalid_feishu_app_parameter">AppId/AppSecret/UserId都不能为空</string>
@ -1198,6 +1201,7 @@
<string name="task_sender">启停通道</string>
<string name="task_sender_tips">控制【发送通道】的启用/禁用</string>
<string name="task_alarm">声音警报</string>
<string name="task_alarm_tips">播放音乐提醒</string>
<string name="second"></string>
<string name="minute"></string>
@ -1354,4 +1358,12 @@
<string name="toast_location_not_enabled">位置服务未开启,请先前往系统设置中开启!</string>
<string name="task_condition_check_again">延迟执行时再次校验</string>
<string name="task_condition_check_again_tips">作为触发条件时,在延迟执行动作时再次校验是否满足</string>
<string name="start_alarm">启动警报</string>
<string name="stop_alarm">停止警报</string>
<string name="alarm_play_settings">播放设置</string>
<string name="alarm_music">指定音乐</string>
<string name="alarm_music_tips">可选,下载 mp3/ogg/wav 到 Download 目录</string>
<string name="alarm_volume">播放音量</string>
<string name="alarm_loop_times">循环次数(0=无限)</string>
</resources>

Loading…
Cancel
Save