diff --git a/app/src/main/java/com/idormy/sms/forwarder/App.kt b/app/src/main/java/com/idormy/sms/forwarder/App.kt index cf2946f3..fced29ff 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/App.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/App.kt @@ -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) diff --git a/app/src/main/java/com/idormy/sms/forwarder/entity/action/AlarmSetting.kt b/app/src/main/java/com/idormy/sms/forwarder/entity/action/AlarmSetting.kt new file mode 100644 index 00000000..5f6222b6 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/entity/action/AlarmSetting.kt @@ -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 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 d3928c2a..f75173b8 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 @@ -194,6 +194,13 @@ class TasksEditFragment : BaseFragment(), 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() { diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/action/AlarmFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/action/AlarmFragment.kt new file mode 100644 index 00000000..67d092de --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/action/AlarmFragment.kt @@ -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(), 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, 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, 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, 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().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, 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, 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, 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 { + val audioFiles = mutableListOf() + 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) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/action/FrpcFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/action/FrpcFragment.kt index 36c4c404..f41a8216 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/fragment/action/FrpcFragment.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/action/FrpcFragment.kt @@ -110,7 +110,7 @@ class FrpcFragment : BaseFragment(), View.OnCli Log.d(TAG, "initViews settingVo:$settingVo") } - //初始化发送通道下拉框 + //初始化Frpc下拉框 initFrpc() } 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 586ebb5e..9397412a 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 @@ -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 { 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(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") 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 15f0a73b..9e2498d2 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 @@ -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" 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 936036ba..cbe94e90 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 @@ -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(EVENT_ALARM_ACTION).post(alarmSetting) + + successNum++ + writeLog(String.format(getString(R.string.successful_execution), alarmSetting.description), "SUCCESS") + } + else -> { writeLog("action.type is ${action.type}") } diff --git a/app/src/main/res/drawable/auto_task_icon_alarm.xml b/app/src/main/res/drawable/auto_task_icon_alarm.xml new file mode 100644 index 00000000..970c7f64 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_alarm.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/drawable/auto_task_icon_alarm_grey.xml b/app/src/main/res/drawable/auto_task_icon_alarm_grey.xml new file mode 100644 index 00000000..fd771d53 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_alarm_grey.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_tasks_action_alarm.xml b/app/src/main/res/layout/fragment_tasks_action_alarm.xml new file mode 100644 index 00000000..9b586118 --- /dev/null +++ b/app/src/main/res/layout/fragment_tasks_action_alarm.xml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/raw/alarm.mp3 b/app/src/main/res/raw/alarm.mp3 new file mode 100644 index 00000000..f14cb155 Binary files /dev/null and b/app/src/main/res/raw/alarm.mp3 differ diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index c45779cb..1e72de7d 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -210,7 +210,7 @@ The log entry is deleted. Are you sure you want to delete all log records for this category? The category log record has been cleared! - Attempting to resend over the original sending channel + Attempting to resend over the original sender Rematching rule sending Details Are you sure to clear all forwarding logs? @@ -436,8 +436,8 @@ SSL Local IP: - 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). - 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). + 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). + 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). Push Pull Stop @@ -589,7 +589,7 @@ %s sec Retry Max Retries - [%s] Congratulations, the sending channel test is successful, please continue to add forwarding rules! + [%s] Congratulations, the sender test is successful, please continue to add forwarding rules! Test Channel SIM1_TestOperator_18888888888 Keep Reminding @@ -822,7 +822,7 @@ Value Add header Del header - Please select send channel type + Please select sender type Group Robot → Webhook Address Group Robot → Security Settings → Signature Verification Please go to the corresponding official website to obtain @@ -976,15 +976,16 @@ Disable this feature on the server Frpc failed to run Successfully deleted - [Note] The sending channel has been disabled, and its associated rules will not be sent even if they match! + [Note] The sender has been disabled, and its associated rules will not be sent even if they match! [Note] The rule has been disabled, will not be sent even if they match! - [Note] The sending channel is already in the list, no need to add it again! + [Note] The sender is already in the list, no need to add it again! [Note] The rule is already in the list, no need to add it again! [Note] The frpc is already in the list, no need to add it again! Local Call: Remote SMS: Clear Unauthorized storage permission, this function cannot be used! + Unauthorized write settings permission, this function cannot be used! Name:%s\nPhone:%s Card slot does not match the rule Unmatched rule @@ -1061,9 +1062,11 @@ Port number value range: 1~65535 ^([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])$ Select Dir + Select File Web Client Restarting HttpServer Download and unzip to:\n%s + Download music file to:\n%s Root Directory:\n%s Select WebClient Directory AppId/AppSecret/UserId cannot be empty @@ -1193,10 +1196,11 @@ Settings Control the configuration switch of "Settings". Rules On/Off - Control enabling/disabling of "Forwarding Rules" + Control enabling/disabling of "Rules" Channels On/Off - Control enabling/disabling of "Sending Channels" + Control enabling/disabling of "Senders" Alarm + Alarm Second Minute @@ -1353,4 +1357,12 @@ Location is not enabled, Please go to system settings and activate it. Recheck when delaying execution. When used as a triggering condition, recheck during delayed action execution. + + Start Alarm + Stop Alarm + Playback Settings + Specify Music + Optional, download mp3/ogg/wav to the Download directory. + Alarm Volume + Loop Times(0=Infinite) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c5a67cc5..2426a053 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -986,6 +986,7 @@ 远程发短信: 清除 未授权储存权限,该功能无法使用! + 未授权修改系统设置权限,该功能无法使用! 姓名:%s\n号码:%s 卡槽未匹配中规则 未匹配中规则 @@ -1062,9 +1063,11 @@ 端口号取值范围:1~65535 ^([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])$ 选择目录 + 选择文件 Web客户端 正在重启HttpServer 请先下载Web客户端并解压到:\n%s + 请先下载音乐文件到:\n%s 根目录:\n%s 选择Web客户端目录 AppId/AppSecret/UserId都不能为空 @@ -1198,6 +1201,7 @@ 启停通道 控制【发送通道】的启用/禁用 声音警报 + 声音警报 @@ -1354,4 +1358,12 @@ 位置服务未开启,请先前往系统设置中开启! 延迟执行时再次校验 作为触发条件时,在延迟执行动作时再次校验是否满足 + + 启动警报 + 停止警报 + 播放设置 + 指定音乐 + 可选,下载 mp3/ogg/wav 到 Download 目录 + 播放音量 + 循环次数(0=无限) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f103a8e5..72ecf1f2 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -986,6 +986,7 @@ 遠程發簡訊: 清除 未授權儲存權限,該功能無法使用! + 未授權修改系統設置權限,該功能無法使用! 姓名:%s\n號碼:%s 卡槽未匹配中規則 未匹配中規則 @@ -1062,9 +1063,11 @@ 端口號取值範圍:1~65535 ^([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])$ 選擇目錄 + 選擇文件 Web客戶端 正在重啟HttpServer 請先下載Web客戶端並解壓到:\n%s + 請先下載音樂文件到:\n%s 根目錄:\n%s 選擇Web客戶端目錄 AppId/AppSecret/UserId都不能為空 @@ -1198,6 +1201,7 @@ 啟停通道 控制【發送通道】的啟用/禁用 聲音警報 + 聲音警報 @@ -1355,4 +1359,12 @@ 定位服務未開啟,請先前往系統設置中開啟! 延遲執行時再次校驗 作為觸發條件時,在延遲執行動作時再次校驗是否滿足 + + 啟動警報 + 停止警報 + 播放設置 + 指定音樂 + 可選,下載 mp3/ogg/wav 到 Download 目錄 + 播放音量 + 循環次數(0=無限) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2430ec30..87ec5e2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -986,6 +986,7 @@ 远程发短信: 清除 未授权储存权限,该功能无法使用! + 未授权修改系统设置权限,该功能无法使用! 姓名:%s\n号码:%s 卡槽未匹配中规则 未匹配中规则 @@ -1062,9 +1063,11 @@ 端口号取值范围:1~65535 ^([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])$ 选择目录 + 选择文件 Web客户端 正在重启HttpServer 请先下载Web客户端并解压到:\n%s + 请先下载音乐文件到:\n%s 根目录:\n%s 选择Web客户端目录 AppId/AppSecret/UserId都不能为空 @@ -1198,6 +1201,7 @@ 启停通道 控制【发送通道】的启用/禁用 声音警报 + 播放音乐提醒 @@ -1354,4 +1358,12 @@ 位置服务未开启,请先前往系统设置中开启! 延迟执行时再次校验 作为触发条件时,在延迟执行动作时再次校验是否满足 + + 启动警报 + 停止警报 + 播放设置 + 指定音乐 + 可选,下载 mp3/ogg/wav 到 Download 目录 + 播放音量 + 循环次数(0=无限)