新增:自动任务·快捷指令 —— Cron定时发送短信 #279 #344

This commit is contained in:
pppscn 2023-12-07 11:07:00 +08:00
parent af63302df6
commit 5fb9f1b75c
14 changed files with 198 additions and 40 deletions

View File

@ -28,7 +28,7 @@ import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.sdkinit.UMengInit import com.idormy.sms.forwarder.utils.sdkinit.UMengInit
import com.idormy.sms.forwarder.utils.sdkinit.XBasicLibInit import com.idormy.sms.forwarder.utils.sdkinit.XBasicLibInit
import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit
import com.idormy.sms.forwarder.utils.task.CronUtils import com.idormy.sms.forwarder.utils.task.AlarmUtils
import com.idormy.sms.forwarder.utils.tinker.TinkerLoadLibrary import com.idormy.sms.forwarder.utils.tinker.TinkerLoadLibrary
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
@ -208,7 +208,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
// 运营统计数据 // 运营统计数据
UMengInit.init(this) UMengInit.init(this)
// 定时任务初始化 // 定时任务初始化
CronUtils.initialize(this) AlarmUtils.initialize(this)
// 三方时间库初始化 // 三方时间库初始化
//AndroidThreeTen.init(this) //AndroidThreeTen.init(this)
} }

View File

@ -16,6 +16,7 @@ import com.idormy.sms.forwarder.adapter.TaskPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Task import com.idormy.sms.forwarder.database.entity.Task
import com.idormy.sms.forwarder.databinding.AdapterTasksCardViewListItemBinding import com.idormy.sms.forwarder.databinding.AdapterTasksCardViewListItemBinding
import com.idormy.sms.forwarder.entity.task.TaskSetting import com.idormy.sms.forwarder.entity.task.TaskSetting
import com.xuexiang.xutil.data.DateUtils
class TaskPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Task, MyViewHolder>(diffCallback) { class TaskPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Task, MyViewHolder>(diffCallback) {
@ -31,6 +32,8 @@ class TaskPagingAdapter(private val itemClickListener: OnItemClickListener) : Pa
if (item.type >= 1000) { if (item.type >= 1000) {
holder.binding.layoutImage.visibility = View.GONE holder.binding.layoutImage.visibility = View.GONE
holder.binding.tvTime.text = DateUtils.getFriendlyTimeSpanByNow(item.lastExecTime.time)
//遍历conditions显示图标 //遍历conditions显示图标
holder.binding.layoutConditionsIcons.removeAllViews() holder.binding.layoutConditionsIcons.removeAllViews()
if (item.conditions.isNotEmpty()) { if (item.conditions.isNotEmpty()) {

View File

@ -175,6 +175,9 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//纯客户端模式 //纯客户端模式
switchDirectlyToClient(binding!!.sbDirectlyToClient) switchDirectlyToClient(binding!!.sbDirectlyToClient)
//纯自动任务模式
switchDirectlyToTask(binding!!.sbDirectlyToTask)
//启用 {{定位信息}} 标签 //启用 {{定位信息}} 标签
switchEnableLocationTag(binding!!.sbEnableLocationTag) switchEnableLocationTag(binding!!.sbEnableLocationTag)
} }
@ -996,6 +999,19 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
} }
} }
//纯自动任务模式
private fun switchDirectlyToTask(@SuppressLint("UseSwitchCompatOrMaterialCode") switchDirectlyToTask: SwitchButton) {
switchDirectlyToTask.isChecked = SettingUtils.enablePureTaskMode
switchDirectlyToTask.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
SettingUtils.enablePureTaskMode = isChecked
if (isChecked) {
MaterialDialog.Builder(requireContext()).content(getString(R.string.enabling_pure_client_mode)).positiveText(R.string.lab_yes).onPositive { _: MaterialDialog?, _: DialogAction? ->
XUtil.exitApp()
}.negativeText(R.string.lab_no).show()
}
}
}
//启用 {{定位信息}} 标签 //启用 {{定位信息}} 标签
private fun switchEnableLocationTag(@SuppressLint("UseSwitchCompatOrMaterialCode") switchEnableLocationTag: SwitchButton) { private fun switchEnableLocationTag(@SuppressLint("UseSwitchCompatOrMaterialCode") switchEnableLocationTag: SwitchButton) {
switchEnableLocationTag.isChecked = SettingUtils.enableLocationTag switchEnableLocationTag.isChecked = SettingUtils.enableLocationTag

View File

@ -257,7 +257,7 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
for (condition in conditionList) { for (condition in conditionList) {
itemListConditions.add(condition) itemListConditions.add(condition)
} }
Log.d(TAG, "initForm: $itemListConditions") Log.d(TAG, "itemListConditions: $itemListConditions")
conditionsAdapter.notifyDataSetChanged() conditionsAdapter.notifyDataSetChanged()
binding!!.layoutAddCondition.visibility = if (itemListConditions.size >= MAX_SETTING_NUM) View.GONE else View.VISIBLE binding!!.layoutAddCondition.visibility = if (itemListConditions.size >= MAX_SETTING_NUM) View.GONE else View.VISIBLE
} }
@ -266,7 +266,7 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
for (action in actionList) { for (action in actionList) {
itemListActions.add(action) itemListActions.add(action)
} }
Log.d(TAG, "initForm: $itemListActions") Log.d(TAG, "itemListActions: $itemListActions")
actionsAdapter.notifyDataSetChanged() actionsAdapter.notifyDataSetChanged()
binding!!.layoutAddAction.visibility = if (itemListActions.size >= MAX_SETTING_NUM) View.GONE else View.VISIBLE binding!!.layoutAddAction.visibility = if (itemListActions.size >= MAX_SETTING_NUM) View.GONE else View.VISIBLE
} }
@ -292,7 +292,9 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
} }
val lastExecTime = Date() val lastExecTime = Date()
var nextExecTime = Date() // 将毫秒部分设置为 0避免因为毫秒部分不同导致的任务重复执行
lastExecTime.time = lastExecTime.time / 1000 * 1000
var nextExecTime = lastExecTime
val firstCondition = itemListConditions[0] val firstCondition = itemListConditions[0]
taskType = firstCondition.type taskType = firstCondition.type
@ -317,15 +319,7 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
val status = if (binding!!.sbStatus.isChecked) STATUS_ON else STATUS_OFF val status = if (binding!!.sbStatus.isChecked) STATUS_ON else STATUS_OFF
return Task( return Task(
taskId, taskId, taskType, taskName, description.toString(), Gson().toJson(itemListConditions), Gson().toJson(itemListActions), status, lastExecTime, nextExecTime
taskType,
taskName,
description.toString(),
Gson().toJson(itemListConditions),
Gson().toJson(itemListActions),
status,
lastExecTime,
nextExecTime
) )
} }
@ -339,11 +333,9 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
//定时任务 //定时任务
TASK_CONDITION_CRON -> { TASK_CONDITION_CRON -> {
//取消旧任务的定时器 & 设置新的定时器 //取消旧任务的定时器 & 设置新的定时器
//CronUtils.cancelAlarm(task) //AlarmUtils.cancelAlarm(task)
//CronUtils.scheduleAlarm(task) //AlarmUtils.scheduleAlarm(task)
//val uuid = App.TaskIdToWorkerIdMap[task.id]
//uuid?.let { CronJobScheduler.cancelTask(it) }
CronJobScheduler.cancelTask(task.id) CronJobScheduler.cancelTask(task.id)
CronJobScheduler.scheduleTask(task) CronJobScheduler.scheduleTask(task)
} }
@ -355,6 +347,10 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
try { try {
dialog.dismiss() dialog.dismiss()
Log.d(TAG, "onItemClick: $widgetInfo") Log.d(TAG, "onItemClick: $widgetInfo")
if (pos > 0) {
XToastUtils.info("暂不支持,敬请期待……")
return
}
//判断点击的是条件还是动作 //判断点击的是条件还是动作
if (widgetInfo.classPath.contains(".condition.")) { if (widgetInfo.classPath.contains(".condition.")) {
//判断是否已经添加过该类型条件 //判断是否已经添加过该类型条件

View File

@ -10,7 +10,7 @@ import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.database.entity.Task import com.idormy.sms.forwarder.database.entity.Task
import com.idormy.sms.forwarder.entity.task.CronSetting import com.idormy.sms.forwarder.entity.task.CronSetting
import com.idormy.sms.forwarder.entity.task.TaskSetting import com.idormy.sms.forwarder.entity.task.TaskSetting
import com.idormy.sms.forwarder.utils.task.CronUtils import com.idormy.sms.forwarder.utils.task.AlarmUtils
import gatewayapps.crondroid.CronExpression import gatewayapps.crondroid.CronExpression
import java.util.Date import java.util.Date
@ -30,7 +30,7 @@ class AlarmReceiver : BroadcastReceiver() {
Log.d(TAG, "lastExecTime = ${task.lastExecTime}, nextExecTime = ${task.nextExecTime}") Log.d(TAG, "lastExecTime = ${task.lastExecTime}, nextExecTime = ${task.nextExecTime}")
try { try {
//取消旧任务的定时器 //取消旧任务的定时器
CronUtils.cancelAlarm(task) AlarmUtils.cancelAlarm(task)
// 根据任务信息执行相应操作 // 根据任务信息执行相应操作
val conditionList = Gson().fromJson(task.conditions, Array<TaskSetting>::class.java).toMutableList() val conditionList = Gson().fromJson(task.conditions, Array<TaskSetting>::class.java).toMutableList()
@ -64,7 +64,7 @@ class AlarmReceiver : BroadcastReceiver() {
return return
} }
//设置新的定时器 //设置新的定时器
CronUtils.scheduleAlarm(task) AlarmUtils.scheduleAlarm(task)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "onReceive error $e") Log.e(TAG, "onReceive error $e")
} }

View File

@ -159,7 +159,7 @@ class SettingUtils private constructor() {
var enablePureClientMode: Boolean by SharedPreference(SP_PURE_CLIENT_MODE, false) var enablePureClientMode: Boolean by SharedPreference(SP_PURE_CLIENT_MODE, false)
//是否纯任务模式 //是否纯任务模式
var enablePureTaskMode: Boolean by SharedPreference(SP_PURE_TASK_MODE, true) var enablePureTaskMode: Boolean by SharedPreference(SP_PURE_TASK_MODE, false)
//是否启用定位标签 //是否启用定位标签
var enableLocationTag: Boolean by SharedPreference(SP_LOCATION_TAG, false) var enableLocationTag: Boolean by SharedPreference(SP_LOCATION_TAG, false)

View File

@ -9,7 +9,7 @@ import android.os.Build
import com.idormy.sms.forwarder.database.entity.Task import com.idormy.sms.forwarder.database.entity.Task
import com.idormy.sms.forwarder.receiver.AlarmReceiver import com.idormy.sms.forwarder.receiver.AlarmReceiver
class CronUtils { class AlarmUtils {
companion object { companion object {
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")

View File

@ -6,7 +6,7 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.idormy.sms.forwarder.database.entity.Task import com.idormy.sms.forwarder.database.entity.Task
import com.idormy.sms.forwarder.workers.TaskWorker import com.idormy.sms.forwarder.workers.CronWorker
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@ -21,13 +21,13 @@ class CronJobScheduler {
val delayInMillis = task.nextExecTime.time / 1000 * 1000 - currentTimeMillis val delayInMillis = task.nextExecTime.time / 1000 * 1000 - currentTimeMillis
val inputData = Data.Builder().putLong("taskId", task.id).build() val inputData = Data.Builder().putLong("taskId", task.id).build()
val taskRequest = if (delayInMillis <= 0L) { val taskRequest = if (delayInMillis <= 0L) {
Log.d(TAG, "立即执行任务${task.id}delayInMillis = $delayInMillis") Log.d(TAG, "任务${task.id}:立即执行delayInMillis = $delayInMillis")
OneTimeWorkRequestBuilder<TaskWorker>() OneTimeWorkRequestBuilder<CronWorker>()
.setInputData(inputData) .setInputData(inputData)
.build() .build()
} else { } else {
Log.d(TAG, "延迟 $delayInMillis 毫秒执行任务${task.id}") Log.d(TAG, "任务${task.id}延迟 $delayInMillis 毫秒执行")
OneTimeWorkRequestBuilder<TaskWorker>() OneTimeWorkRequestBuilder<CronWorker>()
.setInitialDelay(delayInMillis, TimeUnit.MILLISECONDS) .setInitialDelay(delayInMillis, TimeUnit.MILLISECONDS)
.setInputData(inputData) .setInputData(inputData)
.build() .build()

View File

@ -0,0 +1,69 @@
package com.idormy.sms.forwarder.workers
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.google.gson.Gson
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.entity.task.SmsSetting
import com.idormy.sms.forwarder.utils.PhoneUtils
import com.idormy.sms.forwarder.utils.TASK_ACTION_SENDSMS
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xutil.XUtil
@Suppress("PrivatePropertyName", "DEPRECATION")
class ActionWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
private val TAG: String = ActionWorker::class.java.simpleName
override suspend fun doWork(): Result {
val taskId = inputData.getLong("taskId", -1L)
val actionType = inputData.getInt("actionType", -1)
val actionSetting = inputData.getString("actionSetting")
Log.d(TAG, "taskId: $taskId, actionSetting: $actionSetting")
if (taskId == -1L || actionSetting == null) {
Log.d(TAG, "taskId is -1L or actionSetting is null")
return Result.failure()
}
when (actionType) {
TASK_ACTION_SENDSMS -> {
val smsSetting = Gson().fromJson(actionSetting, SmsSetting::class.java)
if (smsSetting == null) {
Log.d(TAG, "任务$taskIdsmsSetting is null")
return Result.failure()
}
//获取卡槽信息
if (App.SimInfoList.isEmpty()) {
App.SimInfoList = PhoneUtils.getSimMultiInfo()
}
Log.d(TAG, App.SimInfoList.toString())
//发送卡槽: 1=SIM1, 2=SIM2
val simSlotIndex = smsSetting.simSlot - 1
//TODO取不到卡槽信息时采用默认卡槽发送
val mSubscriptionId: Int = App.SimInfoList[simSlotIndex]?.mSubscriptionId ?: -1
val msg = if (ActivityCompat.checkSelfPermission(XUtil.getContext(), Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
ResUtils.getString(R.string.no_sms_sending_permission)
} else {
PhoneUtils.sendSms(mSubscriptionId, smsSetting.phoneNumbers, smsSetting.msgContent) ?: "success"
}
Log.d(TAG, "任务$taskIdsend sms result: $msg")
return Result.success()
}
else -> {
Log.d(TAG, "任务$taskIdaction.type is $actionType")
return Result.failure()
}
}
}
}

View File

@ -3,6 +3,9 @@ package com.idormy.sms.forwarder.workers
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.google.gson.Gson import com.google.gson.Gson
import com.idormy.sms.forwarder.App import com.idormy.sms.forwarder.App
@ -11,13 +14,12 @@ import com.idormy.sms.forwarder.entity.task.CronSetting
import com.idormy.sms.forwarder.entity.task.TaskSetting import com.idormy.sms.forwarder.entity.task.TaskSetting
import com.idormy.sms.forwarder.utils.task.CronJobScheduler import com.idormy.sms.forwarder.utils.task.CronJobScheduler
import gatewayapps.crondroid.CronExpression import gatewayapps.crondroid.CronExpression
import kotlinx.coroutines.delay
import java.util.Date import java.util.Date
@Suppress("PrivatePropertyName") @Suppress("PrivatePropertyName", "DEPRECATION")
class TaskWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { class CronWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
private val TAG: String = TaskWorker::class.java.simpleName private val TAG: String = CronWorker::class.java.simpleName
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val taskId = inputData.getLong("taskId", -1L) val taskId = inputData.getLong("taskId", -1L)
@ -27,20 +29,31 @@ class TaskWorker(context: Context, params: WorkerParameters) : CoroutineWorker(c
} }
val task = AppDatabase.getInstance(App.context).taskDao().getOne(taskId) val task = AppDatabase.getInstance(App.context).taskDao().getOne(taskId)
if (task.status == 0) {
Log.d(TAG, "任务${task.id}task is disabled")
return Result.success()
}
// 根据任务信息执行相应操作 // 根据任务信息执行相应操作
val conditionList = Gson().fromJson(task.conditions, Array<TaskSetting>::class.java).toMutableList() val conditionList = Gson().fromJson(task.conditions, Array<TaskSetting>::class.java).toMutableList()
if (conditionList.isEmpty()) { if (conditionList.isEmpty()) {
Log.d(TAG, "onReceive conditionList is empty") Log.d(TAG, "任务${task.id}conditionList is empty")
return Result.failure() return Result.failure()
} }
val firstCondition = conditionList.firstOrNull() val firstCondition = conditionList.firstOrNull()
if (firstCondition == null) { if (firstCondition == null) {
Log.d(TAG, "onReceive firstCondition is null") Log.d(TAG, "任务${task.id}firstCondition is null")
return Result.failure() return Result.failure()
} }
val cronSetting = Gson().fromJson(firstCondition.setting, CronSetting::class.java) val cronSetting = Gson().fromJson(firstCondition.setting, CronSetting::class.java)
if (cronSetting == null) { if (cronSetting == null) {
Log.d(TAG, "onReceive cronSetting is null") Log.d(TAG, "任务${task.id}cronSetting is null")
return Result.failure()
}
// TODO: 判断其他条件是否满足
if (false) {
Log.d(TAG, "任务${task.id}:其他条件不满足")
return Result.failure() return Result.failure()
} }
@ -52,7 +65,7 @@ class TaskWorker(context: Context, params: WorkerParameters) : CoroutineWorker(c
// 将 nextExecTime 的毫秒部分设置为 0避免因为毫秒部分不同导致的任务重复执行 // 将 nextExecTime 的毫秒部分设置为 0避免因为毫秒部分不同导致的任务重复执行
nextExecTime.time = nextExecTime.time / 1000 * 1000 nextExecTime.time = nextExecTime.time / 1000 * 1000
task.nextExecTime = nextExecTime task.nextExecTime = nextExecTime
Log.d(TAG, "lastExecTime = ${task.lastExecTime}, nextExecTime = ${task.nextExecTime}") Log.d(TAG, "任务${task.id}lastExecTime = ${task.lastExecTime}, nextExecTime = ${task.nextExecTime}")
// 自动禁用任务 // 自动禁用任务
if (task.nextExecTime.time / 1000 < now.time / 1000) { if (task.nextExecTime.time / 1000 < now.time / 1000) {
@ -63,13 +76,25 @@ class TaskWorker(context: Context, params: WorkerParameters) : CoroutineWorker(c
AppDatabase.getInstance(App.context).taskDao().updateExecTime(task.id, task.lastExecTime, task.nextExecTime, task.status) AppDatabase.getInstance(App.context).taskDao().updateExecTime(task.id, task.lastExecTime, task.nextExecTime, task.status)
if (task.status == 0) { if (task.status == 0) {
Log.d(TAG, "onReceive task is disabled") Log.d(TAG, "任务${task.id}task is disabled")
return Result.success() return Result.success()
} }
// TODO: 执行具体任务 // TODO: 执行具体任务
Log.d(TAG, "执行具体任务,耗时 200 毫秒") val actionList = Gson().fromJson(task.actions, Array<TaskSetting>::class.java).toMutableList()
delay(200L) if (actionList.isEmpty()) {
Log.d(TAG, "任务${task.id}actionsList is empty")
return Result.failure()
}
for (action in actionList) {
val actionData = Data.Builder()
.putLong("taskId", task.id)
.putInt("actionType", action.type)
.putString("actionSetting", action.setting)
.build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
}
// 为新的 nextExecTime 调度下一次任务执行 // 为新的 nextExecTime 调度下一次任务执行
CronJobScheduler.cancelTask(task.id) CronJobScheduler.cancelTask(task.id)

View File

@ -39,9 +39,16 @@
<LinearLayout <LinearLayout
android:id="@+id/layout_actions_icons" android:id="@+id/layout_actions_icons"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" /> android:layout_marginStart="5dp" />
</LinearLayout> </LinearLayout>

View File

@ -1748,6 +1748,40 @@
</LinearLayout> </LinearLayout>
<LinearLayout
style="@style/settingBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pure_task_mode"
android:textStyle="bold"
tools:ignore="RelativeOverlap" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pure_task_mode_tips"
android:textSize="9sp"
tools:ignore="SmallSp" />
</LinearLayout>
<com.xuexiang.xui.widget.button.switchbutton.SwitchButton
android:id="@+id/sb_directly_to_task"
style="@style/SwitchButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -989,6 +989,10 @@
<string name="pure_client_mode_tips">When starting the APP, it will directly enter the active control client</string> <string name="pure_client_mode_tips">When starting the APP, it will directly enter the active control client</string>
<string name="exit_pure_client_mode">Exit pure client mode</string> <string name="exit_pure_client_mode">Exit pure client mode</string>
<string name="enabling_pure_client_mode">Do you want to quit the app immediately and start it manually to take effect in pure client mode?</string> <string name="enabling_pure_client_mode">Do you want to quit the app immediately and start it manually to take effect in pure client mode?</string>
<string name="pure_task_mode">Directly To Task</string>
<string name="pure_task_mode_tips">When starting the APP, it will directly enter the task center</string>
<string name="exit_pure_task_mode">Exit pure task mode</string>
<string name="enabling_pure_task_mode">Do you want to quit the app immediately and start it manually to take effect in pure task mode?</string>
<string name="optional_components">Optional:</string> <string name="optional_components">Optional:</string>
<string name="enable_cactus">Enable Cactus Keep Alive</string> <string name="enable_cactus">Enable Cactus Keep Alive</string>
<string name="enabe_cactus_tips">Dual process foreground service/JobScheduler/WorkManager/1px/silent music</string> <string name="enabe_cactus_tips">Dual process foreground service/JobScheduler/WorkManager/1px/silent music</string>

View File

@ -990,6 +990,10 @@
<string name="pure_client_mode_tips">启动APP时直接进入主动控制·客户端</string> <string name="pure_client_mode_tips">启动APP时直接进入主动控制·客户端</string>
<string name="exit_pure_client_mode">退出纯客户端模式</string> <string name="exit_pure_client_mode">退出纯客户端模式</string>
<string name="enabling_pure_client_mode">是否立即退出App并手动启动以生效纯客户端模式</string> <string name="enabling_pure_client_mode">是否立即退出App并手动启动以生效纯客户端模式</string>
<string name="pure_task_mode">纯自动任务模式</string>
<string name="pure_task_mode_tips">启动APP时直接进入自动任务</string>
<string name="exit_pure_task_mode">退出纯自动任务模式</string>
<string name="enabling_pure_task_mode">是否立即退出App并手动启动以生效纯自动任务模式</string>
<string name="optional_components">可选组件:</string> <string name="optional_components">可选组件:</string>
<string name="enable_cactus">启用 Cactus 增强保活措施(会增加耗电)</string> <string name="enable_cactus">启用 Cactus 增强保活措施(会增加耗电)</string>
<string name="enabe_cactus_tips">双进程前台服务/JobScheduler/WorkManager/1像素/无声音乐</string> <string name="enabe_cactus_tips">双进程前台服务/JobScheduler/WorkManager/1像素/无声音乐</string>