From d10d831685292ba78e8c0c569489c9ead57b2912 Mon Sep 17 00:00:00 2001 From: pppscn <35696959@qq.com> Date: Fri, 1 Dec 2023 22:37:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=C2=B7=E5=BF=AB=E6=8D=B7=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 25 +- app/src/main/assets/tips.json | 6 +- .../main/java/com/idormy/sms/forwarder/App.kt | 5 + .../sms/forwarder/activity/MainActivity.kt | 2 +- .../forwarder/adapter/TaskPagingAdapter.kt | 65 + .../adapter/spinner/ActionAdapterItem.kt | 92 + .../adapter/spinner/ConditionAdapterItem.kt | 92 + .../com/idormy/sms/forwarder/core/Core.kt | 4 +- .../sms/forwarder/database/AppDatabase.kt | 34 +- .../sms/forwarder/database/dao/TaskDao.kt | 52 + .../sms/forwarder/database/entity/Task.kt | 69 + .../database/repository/TaskRepository.kt | 30 + .../viewmodel/BaseViewModelFactory.kt | 10 + .../database/viewmodel/TaskViewModel.kt | 39 + .../idormy/sms/forwarder/entity/CloneInfo.kt | 4 + .../sms/forwarder/entity/task/CronSetting.kt | 8 + .../sms/forwarder/entity/task/TaskSetting.kt | 24 + .../forwarder/fragment/RulesEditFragment.kt | 2 +- .../forwarder/fragment/TasksEditFragment.kt | 352 ++++ .../sms/forwarder/fragment/TasksFragment.kt | 129 ++ .../fragment/condition/CronFragment.kt | 1650 +++++++++++++++++ .../sms/forwarder/receiver/AlarmReceiver.kt | 23 + .../idormy/sms/forwarder/utils/Constants.kt | 55 +- .../sms/forwarder/utils/HttpServerUtils.kt | 8 + .../sms/forwarder/utils/task/CronUtils.kt | 66 + .../res/drawable/auto_task_icon_battery.xml | 15 + .../res/drawable/auto_task_icon_bluetooth.xml | 15 + .../auto_task_icon_bluetooth_device.xml | 19 + .../res/drawable/auto_task_icon_charge.xml | 15 + .../drawable/auto_task_icon_connect_wlan.xml | 35 + .../main/res/drawable/auto_task_icon_cron.xml | 15 + ..._task_icon_disconnect_bluetooth_device.xml | 22 + .../auto_task_icon_disconnect_wlan.xml | 39 + .../res/drawable/auto_task_icon_incall.xml | 15 + .../drawable/auto_task_icon_leave_address.xml | 19 + .../res/drawable/auto_task_icon_location.xml | 14 + .../main/res/drawable/auto_task_icon_sim.xml | 15 + .../drawable/auto_task_icon_to_address.xml | 19 + .../main/res/drawable/auto_task_icon_wlan.xml | 27 + app/src/main/res/drawable/ic_drag.xml | 9 + app/src/main/res/drawable/ic_edit.xml | 3 +- app/src/main/res/drawable/ic_menu_task.xml | 9 + app/src/main/res/drawable/ic_sim.xml | 22 +- app/src/main/res/drawable/ic_sim1.xml | 16 +- app/src/main/res/drawable/ic_sim2.xml | 16 +- .../res/layout/adapter_task_setting_item.xml | 86 + .../adapter_tasks_card_view_list_item.xml | 82 + .../dialog_task_action_bottom_sheet.xml | 28 + .../dialog_task_condition_bottom_sheet.xml | 28 + app/src/main/res/layout/fragment_tasks.xml | 48 + .../main/res/layout/fragment_tasks_cron.xml | 1259 +++++++++++++ .../main/res/layout/fragment_tasks_edit.xml | 265 +++ app/src/main/res/layout/item_add_action.xml | 68 + .../main/res/layout/item_add_condition.xml | 68 + app/src/main/res/menu/menu_drawer.xml | 6 + app/src/main/res/values-en/arrays.xml | 6 + app/src/main/res/values-en/strings.xml | 85 +- app/src/main/res/values/arrays.xml | 6 + app/src/main/res/values/strings.xml | 85 +- app/src/main/res/values/styles_widget.xml | 23 + 60 files changed, 5288 insertions(+), 60 deletions(-) create mode 100644 app/src/main/java/com/idormy/sms/forwarder/adapter/TaskPagingAdapter.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/adapter/spinner/ActionAdapterItem.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/adapter/spinner/ConditionAdapterItem.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/database/dao/TaskDao.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/database/entity/Task.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/database/repository/TaskRepository.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/database/viewmodel/TaskViewModel.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/entity/task/CronSetting.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/entity/task/TaskSetting.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/fragment/TasksEditFragment.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/fragment/TasksFragment.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/fragment/condition/CronFragment.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/receiver/AlarmReceiver.kt create mode 100644 app/src/main/java/com/idormy/sms/forwarder/utils/task/CronUtils.kt create mode 100644 app/src/main/res/drawable/auto_task_icon_battery.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_bluetooth.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_bluetooth_device.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_charge.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_connect_wlan.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_cron.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_disconnect_bluetooth_device.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_disconnect_wlan.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_incall.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_leave_address.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_location.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_sim.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_to_address.xml create mode 100644 app/src/main/res/drawable/auto_task_icon_wlan.xml create mode 100644 app/src/main/res/drawable/ic_drag.xml create mode 100644 app/src/main/res/drawable/ic_menu_task.xml create mode 100644 app/src/main/res/layout/adapter_task_setting_item.xml create mode 100644 app/src/main/res/layout/adapter_tasks_card_view_list_item.xml create mode 100644 app/src/main/res/layout/dialog_task_action_bottom_sheet.xml create mode 100644 app/src/main/res/layout/dialog_task_condition_bottom_sheet.xml create mode 100644 app/src/main/res/layout/fragment_tasks.xml create mode 100644 app/src/main/res/layout/fragment_tasks_cron.xml create mode 100644 app/src/main/res/layout/fragment_tasks_edit.xml create mode 100644 app/src/main/res/layout/item_add_action.xml create mode 100644 app/src/main/res/layout/item_add_condition.xml diff --git a/app/build.gradle b/app/build.gradle index 8ccccd8a..db19973b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,14 +161,13 @@ android { android.applicationVariants.all { variant -> // Assigns a different version code for each output APK. - variant.outputs.each { - output -> - def date = new Date().format("yyyyMMdd", TimeZone.getTimeZone("GMT+08")) - //noinspection GrDeprecatedAPIUsage - def abiName = output.getFilter(com.android.build.OutputFile.ABI) - if (abiName == null) abiName = "universal" - output.versionCodeOverride = abiCodes.get(abiName, 0) * 100000 + variant.versionCode - output.outputFileName = "SmsF_${versionName}_${output.versionCode}_${abiName}_${date}_${variant.name}.apk" + variant.outputs.each { output -> + def date = new Date().format("yyyyMMdd", TimeZone.getTimeZone("GMT+08")) + //noinspection GrDeprecatedAPIUsage + def abiName = output.getFilter(com.android.build.OutputFile.ABI) + if (abiName == null) abiName = "universal" + output.versionCodeOverride = abiCodes.get(abiName, 0) * 100000 + variant.versionCode + output.outputFileName = "SmsF_${versionName}_${output.versionCode}_${abiName}_${date}_${variant.name}.apk" } } @@ -283,6 +282,16 @@ dependencies { //Location 是一个通过 Android 自带的 LocationManager 来实现的定位功能:https://github.com/jenly1314/Location implementation 'com.github.pppscn:location:1.0.0' + + //crontab解析库:https://github.com/jmrozanec/cron-utils 官网:http://cron-parser.com + //implementation 'com.cronutils:cron-utils:9.2.1' + //JSR-310 backport for Android:https://github.com/JakeWharton/ThreeTenABP + //implementation 'com.jakewharton.threetenabp:threetenabp:1.4.6' + + //Partial implementation of Quartz Cron Java for Android: https://github.com/gatewayapps/crondroid + implementation 'gatewayapps.crondroid:crondroid:1.0.0' + //Java Parser For Cron Expressions: https://github.com/grahamar/cron-parser + implementation 'net.redhogs.cronparser:cron-parser-core:3.5' } //自动添加X-Library依赖 apply from: 'x-library.gradle' diff --git a/app/src/main/assets/tips.json b/app/src/main/assets/tips.json index f0a67e6e..bb6b0ea0 100644 --- a/app/src/main/assets/tips.json +++ b/app/src/main/assets/tips.json @@ -3,11 +3,11 @@ "Data": [ { "title": "新用户必读", - "content": "开始设置之前,请您认真地看一遍 Wiki
\n遇到问题,请按照 常见问题 章节进行排查!
\n没找到答案的,再加入QQ互助交流群里提问,请清楚地描述问题,并给出对应的配置截图与相关日志,方便大家直观的判断问题! " + "content": "开始设置之前,请您认真地看一遍 Wiki
\n遇到问题,请按照 常见问题 章节进行排查!
\n没找到答案的,再加入互助交流群提问,请清楚地描述问题,并给出对应的配置截图与相关日志,方便大家直观的判断问题! " }, { - "title": "互助交流群", - "content": "QQ互助交流①群
QQ互助交流②群
QQ互助交流③群
QQ互助交流④群
QQ互助交流⑤群" + "title": "SmsF 互助交流群", + "content": "QQ频道号: q7oofwp13s
Telegram 群组
钉钉群号: 29760014208" }, { "title": "打赏名单", 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 76966de7..620309c0 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/App.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/App.kt @@ -28,6 +28,7 @@ import com.idormy.sms.forwarder.utils.* import com.idormy.sms.forwarder.utils.sdkinit.UMengInit import com.idormy.sms.forwarder.utils.sdkinit.XBasicLibInit import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit +import com.idormy.sms.forwarder.utils.task.CronUtils import com.idormy.sms.forwarder.utils.tinker.TinkerLoadLibrary import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers @@ -206,6 +207,10 @@ class App : Application(), CactusCallback, Configuration.Provider by Core { XUpdateInit.init(this) // 运营统计数据 UMengInit.init(this) + // 定时任务初始化 + CronUtils.initialize(this) + // 三方时间库初始化 + //AndroidThreeTen.init(this) } @SuppressLint("CheckResult") diff --git a/app/src/main/java/com/idormy/sms/forwarder/activity/MainActivity.kt b/app/src/main/java/com/idormy/sms/forwarder/activity/MainActivity.kt index c5cf9217..de5bc08d 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/activity/MainActivity.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/activity/MainActivity.kt @@ -17,7 +17,6 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetDialog -import com.gyf.cactus.ext.cactusUpdateNotification import com.hjq.permissions.OnPermissionCallback import com.hjq.permissions.Permission import com.hjq.permissions.XXPermissions @@ -169,6 +168,7 @@ class MainActivity : BaseActivity(), return@setNavigationItemSelectedListener handleNavigationItemSelected(menuItem) } else { when (menuItem.itemId) { + R.id.nav_task -> openNewPage(TasksFragment::class.java) R.id.nav_server -> openNewPage(ServerFragment::class.java) R.id.nav_client -> openNewPage(ClientFragment::class.java) R.id.nav_frpc -> { diff --git a/app/src/main/java/com/idormy/sms/forwarder/adapter/TaskPagingAdapter.kt b/app/src/main/java/com/idormy/sms/forwarder/adapter/TaskPagingAdapter.kt new file mode 100644 index 00000000..fd2eee5a --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/adapter/TaskPagingAdapter.kt @@ -0,0 +1,65 @@ +package com.idormy.sms.forwarder.adapter + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.idormy.sms.forwarder.R +import com.idormy.sms.forwarder.adapter.TaskPagingAdapter.MyViewHolder +import com.idormy.sms.forwarder.database.entity.Task +import com.idormy.sms.forwarder.databinding.AdapterTasksCardViewListItemBinding + +class TaskPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter(diffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { + val binding = AdapterTasksCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return MyViewHolder(binding) + } + + override fun onBindViewHolder(holder: MyViewHolder, position: Int) { + val item = getItem(position) + if (item != null) { + holder.binding.ivImage.setImageResource(item.imageId) + holder.binding.ivStatus.setImageResource(item.statusImageId) + holder.binding.tvName.text = item.name + + /*holder.binding.cardView.setOnClickListener { view: View? -> + itemClickListener.onItemClicked(view, item) + }*/ + holder.binding.ivCopy.setImageResource(R.drawable.ic_copy) + holder.binding.ivEdit.setImageResource(R.drawable.ic_edit) + holder.binding.ivDelete.setImageResource(R.drawable.ic_delete) + holder.binding.ivCopy.setOnClickListener { view: View? -> + itemClickListener.onItemClicked(view, item) + } + holder.binding.ivEdit.setOnClickListener { view: View? -> + itemClickListener.onItemClicked(view, item) + } + holder.binding.ivDelete.setOnClickListener { view: View? -> + itemClickListener.onItemClicked(view, item) + } + } + } + + class MyViewHolder(val binding: AdapterTasksCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root) + interface OnItemClickListener { + fun onItemClicked(view: View?, item: Task) + fun onItemRemove(view: View?, id: Int) + } + + companion object { + var diffCallback: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean { + return oldItem.id == newItem.id + } + + @SuppressLint("DiffUtilEquals") + override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean { + return oldItem === newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/adapter/spinner/ActionAdapterItem.kt b/app/src/main/java/com/idormy/sms/forwarder/adapter/spinner/ActionAdapterItem.kt new file mode 100644 index 00000000..c3fd14b5 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/adapter/spinner/ActionAdapterItem.kt @@ -0,0 +1,92 @@ +package com.idormy.sms.forwarder.adapter.spinner + +import android.content.Context +import android.graphics.drawable.Drawable +import com.xuexiang.xui.utils.ResUtils + +@Suppress("unused") +class ActionAdapterItem { + + //标题内容 + var title: CharSequence + + //图标 + var icon: Drawable? = null + + //ID + var id: Long? = 0L + + //状态 + var status: Int? = 1 + + constructor(title: CharSequence) { + this.title = title + } + + constructor(title: CharSequence, icon: Drawable?) { + this.title = title + this.icon = icon + } + + constructor(title: CharSequence, icon: Drawable?, id: Long?) { + this.title = title + this.icon = icon + this.id = id + } + + constructor(title: CharSequence, icon: Drawable?, id: Long?, status: Int?) { + this.title = title + this.icon = icon + this.id = id + this.status = status + } + + constructor(title: CharSequence, drawableId: Int) : this(title, ResUtils.getDrawable(drawableId)) + constructor(title: CharSequence, drawableId: Int, id: Long) : this(title, ResUtils.getDrawable(drawableId), id) + constructor(title: CharSequence, drawableId: Int, id: Long, status: Int) : this(title, ResUtils.getDrawable(drawableId), id, status) + constructor(context: Context?, titleId: Int, drawableId: Int) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId)) + constructor(context: Context?, titleId: Int, drawableId: Int, id: Long) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId), id) + constructor(context: Context?, titleId: Int, drawableId: Int, id: Long, status: Int) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId), id, status) + constructor(context: Context?, title: CharSequence, drawableId: Int) : this(title, ResUtils.getDrawable(context, drawableId)) + constructor(context: Context?, title: CharSequence, drawableId: Int, id: Long) : this(title, ResUtils.getDrawable(context, drawableId), id) + constructor(context: Context?, title: CharSequence, drawableId: Int, id: Long, status: Int) : this(title, ResUtils.getDrawable(context, drawableId), id, status) + + fun setStatus(status: Int): ActionAdapterItem { + this.status = status + return this + } + + fun setId(id: Long): ActionAdapterItem { + this.id = id + return this + } + + fun setTitle(title: CharSequence): ActionAdapterItem { + this.title = title + return this + } + + fun setIcon(icon: Drawable?): ActionAdapterItem { + this.icon = icon + return this + } + + //注意:自定义实体需要重写对象的toString方法 + override fun toString(): String { + return title.toString() + } + + companion object { + fun of(title: CharSequence): ActionAdapterItem { + return ActionAdapterItem(title) + } + + fun arrayof(title: Array): Array { + val array = arrayOfNulls(title.size) + for (i in array.indices) { + array[i] = ActionAdapterItem(title[i]) + } + return array + } + } +} diff --git a/app/src/main/java/com/idormy/sms/forwarder/adapter/spinner/ConditionAdapterItem.kt b/app/src/main/java/com/idormy/sms/forwarder/adapter/spinner/ConditionAdapterItem.kt new file mode 100644 index 00000000..5bb30eb2 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/adapter/spinner/ConditionAdapterItem.kt @@ -0,0 +1,92 @@ +package com.idormy.sms.forwarder.adapter.spinner + +import android.content.Context +import android.graphics.drawable.Drawable +import com.xuexiang.xui.utils.ResUtils + +@Suppress("unused") +class ConditionAdapterItem { + + //标题内容 + var title: CharSequence + + //图标 + var icon: Drawable? = null + + //ID + var id: Long? = 0L + + //状态 + var status: Int? = 1 + + constructor(title: CharSequence) { + this.title = title + } + + constructor(title: CharSequence, icon: Drawable?) { + this.title = title + this.icon = icon + } + + constructor(title: CharSequence, icon: Drawable?, id: Long?) { + this.title = title + this.icon = icon + this.id = id + } + + constructor(title: CharSequence, icon: Drawable?, id: Long?, status: Int?) { + this.title = title + this.icon = icon + this.id = id + this.status = status + } + + constructor(title: CharSequence, drawableId: Int) : this(title, ResUtils.getDrawable(drawableId)) + constructor(title: CharSequence, drawableId: Int, id: Long) : this(title, ResUtils.getDrawable(drawableId), id) + constructor(title: CharSequence, drawableId: Int, id: Long, status: Int) : this(title, ResUtils.getDrawable(drawableId), id, status) + constructor(context: Context?, titleId: Int, drawableId: Int) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId)) + constructor(context: Context?, titleId: Int, drawableId: Int, id: Long) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId), id) + constructor(context: Context?, titleId: Int, drawableId: Int, id: Long, status: Int) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId), id, status) + constructor(context: Context?, title: CharSequence, drawableId: Int) : this(title, ResUtils.getDrawable(context, drawableId)) + constructor(context: Context?, title: CharSequence, drawableId: Int, id: Long) : this(title, ResUtils.getDrawable(context, drawableId), id) + constructor(context: Context?, title: CharSequence, drawableId: Int, id: Long, status: Int) : this(title, ResUtils.getDrawable(context, drawableId), id, status) + + fun setStatus(status: Int): ConditionAdapterItem { + this.status = status + return this + } + + fun setId(id: Long): ConditionAdapterItem { + this.id = id + return this + } + + fun setTitle(title: CharSequence): ConditionAdapterItem { + this.title = title + return this + } + + fun setIcon(icon: Drawable?): ConditionAdapterItem { + this.icon = icon + return this + } + + //注意:自定义实体需要重写对象的toString方法 + override fun toString(): String { + return title.toString() + } + + companion object { + fun of(title: CharSequence): ConditionAdapterItem { + return ConditionAdapterItem(title) + } + + fun arrayof(title: Array): Array { + val array = arrayOfNulls(title.size) + for (i in array.indices) { + array[i] = ConditionAdapterItem(title[i]) + } + return array + } + } +} diff --git a/app/src/main/java/com/idormy/sms/forwarder/core/Core.kt b/app/src/main/java/com/idormy/sms/forwarder/core/Core.kt index 5917aabf..4bdc512c 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/core/Core.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/core/Core.kt @@ -1,14 +1,11 @@ package com.idormy.sms.forwarder.core import android.app.Application -import android.content.Intent import android.util.Log -import androidx.core.content.ContextCompat import androidx.work.Configuration import com.idormy.sms.forwarder.App import com.idormy.sms.forwarder.BuildConfig import com.idormy.sms.forwarder.database.repository.* -import com.idormy.sms.forwarder.service.ForegroundService import kotlinx.coroutines.launch @Suppress("unused") @@ -19,6 +16,7 @@ object Core : Configuration.Provider { val logs: LogsRepository by lazy { (app as App).logsRepository } val rule: RuleRepository by lazy { (app as App).ruleRepository } val sender: SenderRepository by lazy { (app as App).senderRepository } + val task: TaskRepository by lazy { (app as App).taskRepository } fun init(app: Application) { this.app = app diff --git a/app/src/main/java/com/idormy/sms/forwarder/database/AppDatabase.kt b/app/src/main/java/com/idormy/sms/forwarder/database/AppDatabase.kt index 5f54c88c..6eb5d9c0 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/database/AppDatabase.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/database/AppDatabase.kt @@ -13,7 +13,7 @@ import com.idormy.sms.forwarder.database.ext.ConvertersDate import com.idormy.sms.forwarder.utils.DATABASE_NAME @Database( - entities = [Frpc::class, Msg::class, Logs::class, Rule::class, Sender::class], + entities = [Frpc::class, Msg::class, Logs::class, Rule::class, Sender::class, Task::class], views = [LogsDetail::class], version = 18, exportSchema = false @@ -26,6 +26,7 @@ abstract class AppDatabase : RoomDatabase() { abstract fun logsDao(): LogsDao abstract fun ruleDao(): RuleDao abstract fun senderDao(): SenderDao + abstract fun taskDao(): TaskDao companion object { @Volatile @@ -39,11 +40,8 @@ abstract class AppDatabase : RoomDatabase() { private fun buildDatabase(context: Context): AppDatabase { val builder = Room.databaseBuilder( - context.applicationContext, - AppDatabase::class.java, - DATABASE_NAME - ) - .allowMainThreadQueries() //TODO:允许主线程访问,后面再优化 + context.applicationContext, AppDatabase::class.java, DATABASE_NAME + ).allowMainThreadQueries() //TODO:允许主线程访问,后面再优化 .addCallback(object : Callback() { override fun onCreate(db: SupportSQLiteDatabase) { //fillInDb(context.applicationContext) @@ -80,8 +78,7 @@ custom_domains = smsf.demo.com """.trimIndent() ) } - }) - .addMigrations( + }).addMigrations( MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, @@ -98,7 +95,7 @@ custom_domains = smsf.demo.com MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, - MIGRATION_17_18 + MIGRATION_17_18, ) /*if (BuildConfig.DEBUG) { @@ -382,10 +379,25 @@ CREATE TABLE "Logs" ( } } - //规则配置增加uid条件 + //自动化任务 private val MIGRATION_17_18 = object : Migration(17, 18) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("Alter table rule add column uid INTEGER NOT NULL DEFAULT 0 ") + database.execSQL( + """ +CREATE TABLE "Task" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "type" INTEGER NOT NULL DEFAULT 1, + "name" TEXT NOT NULL DEFAULT '', + "description" TEXT NOT NULL DEFAULT '', + "conditions" TEXT NOT NULL DEFAULT '', + "actions" TEXT NOT NULL DEFAULT '', + "last_exec_time" INTEGER NOT NULL, + "next_exec_time" INTEGER NOT NULL, + "status" INTEGER NOT NULL DEFAULT 1 +) +""".trimIndent() + ) + //TODO:原来的电量/网络/SIM卡状态转换为自动化任务 } } diff --git a/app/src/main/java/com/idormy/sms/forwarder/database/dao/TaskDao.kt b/app/src/main/java/com/idormy/sms/forwarder/database/dao/TaskDao.kt new file mode 100644 index 00000000..3d4f3892 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/database/dao/TaskDao.kt @@ -0,0 +1,52 @@ +package com.idormy.sms.forwarder.database.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import androidx.room.RawQuery +import androidx.room.Transaction +import androidx.room.Update +import androidx.sqlite.db.SupportSQLiteQuery +import com.idormy.sms.forwarder.database.entity.Task +import io.reactivex.Single + +@Dao +interface TaskDao { + + @Query("SELECT * FROM Task where id=:id") + fun get(id: Long): Single + + @Query("SELECT * FROM Task where id=:id") + fun getOne(id: Long): Task + + @Query("SELECT * FROM Task ORDER BY id DESC") + fun getAll(): List + + @Query("SELECT * FROM Task where type=:type ORDER BY id DESC") + fun pagingSource(type: String): PagingSource + + @Transaction + @RawQuery(observedEntities = [Task::class]) + fun getAllRaw(query: SupportSQLiteQuery): List + + @Query("SELECT * FROM Task WHERE type = :taskType") + fun getByType(taskType: String): List + + //TODO:根据条件查询,不推荐使用 + @Query("SELECT * FROM Task WHERE type = :taskType AND conditions LIKE '%' || :conditionKey || '%' AND conditions LIKE '%' || :conditionValue || '%'") + fun getByCondition(taskType: String, conditionKey: String, conditionValue: String): List + + @Insert + fun insert(task: Task) + + @Update + fun update(task: Task) + + @Query("DELETE FROM Task WHERE id = :taskId") + fun delete(taskId: Long) + + @Query("DELETE FROM Task") + fun deleteAll() + +} diff --git a/app/src/main/java/com/idormy/sms/forwarder/database/entity/Task.kt b/app/src/main/java/com/idormy/sms/forwarder/database/entity/Task.kt new file mode 100644 index 00000000..51444889 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/database/entity/Task.kt @@ -0,0 +1,69 @@ +package com.idormy.sms.forwarder.database.entity + +import android.os.Parcelable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.idormy.sms.forwarder.R +import com.idormy.sms.forwarder.utils.STATUS_OFF +import com.idormy.sms.forwarder.utils.TYPE_BARK +import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_GROUP_ROBOT +import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_INNER_ROBOT +import com.idormy.sms.forwarder.utils.TYPE_EMAIL +import com.idormy.sms.forwarder.utils.TYPE_FEISHU +import com.idormy.sms.forwarder.utils.TYPE_FEISHU_APP +import com.idormy.sms.forwarder.utils.TYPE_GOTIFY +import com.idormy.sms.forwarder.utils.TYPE_PUSHPLUS +import com.idormy.sms.forwarder.utils.TYPE_SERVERCHAN +import com.idormy.sms.forwarder.utils.TYPE_SMS +import com.idormy.sms.forwarder.utils.TYPE_SOCKET +import com.idormy.sms.forwarder.utils.TYPE_TELEGRAM +import com.idormy.sms.forwarder.utils.TYPE_URL_SCHEME +import com.idormy.sms.forwarder.utils.TYPE_WEBHOOK +import com.idormy.sms.forwarder.utils.TYPE_WEWORK_AGENT +import com.idormy.sms.forwarder.utils.TYPE_WEWORK_ROBOT +import kotlinx.parcelize.Parcelize +import java.util.Date + +@Parcelize +@Entity(tableName = "Task") +data class Task( + @PrimaryKey(autoGenerate = true) var id: Long = 0, + @ColumnInfo(name = "type", defaultValue = "1") var type: Int = 1, // 任务类型字段 + @ColumnInfo(name = "name", defaultValue = "") val name: String = "", // 任务名称 + @ColumnInfo(name = "description", defaultValue = "") val description: String = "", // 任务描述 + @ColumnInfo(name = "conditions", defaultValue = "") val conditions: String = "", // 触发条件 + @ColumnInfo(name = "actions", defaultValue = "") val actions: String = "", // 执行动作 + @ColumnInfo(name = "last_exec_time") var lastExecTime: Date = Date(), // 上次执行时间 + @ColumnInfo(name = "next_exec_time") var nextExecTime: Date = Date(), // 下次执行时间 + @ColumnInfo(name = "status", defaultValue = "1") var status: Int = 1, // 任务状态 +) : Parcelable { + + val imageId: Int + get() = when (type) { + TYPE_DINGTALK_GROUP_ROBOT -> R.drawable.icon_dingtalk + TYPE_EMAIL -> R.drawable.icon_email + TYPE_BARK -> R.drawable.icon_bark + TYPE_WEBHOOK -> R.drawable.icon_webhook + TYPE_WEWORK_ROBOT -> R.drawable.icon_wework_robot + TYPE_WEWORK_AGENT -> R.drawable.icon_wework_agent + TYPE_SERVERCHAN -> R.drawable.icon_serverchan + TYPE_TELEGRAM -> R.drawable.icon_telegram + TYPE_FEISHU -> R.drawable.icon_feishu + TYPE_PUSHPLUS -> R.drawable.icon_pushplus + TYPE_GOTIFY -> R.drawable.icon_gotify + TYPE_SMS -> R.drawable.icon_sms + TYPE_DINGTALK_INNER_ROBOT -> R.drawable.icon_dingtalk_inner + TYPE_FEISHU_APP -> R.drawable.icon_feishu_app + TYPE_URL_SCHEME -> R.drawable.icon_url_scheme + TYPE_SOCKET -> R.drawable.icon_socket + else -> R.drawable.icon_sms + } + + val statusImageId: Int + get() = when (status) { + STATUS_OFF -> R.drawable.icon_off + else -> R.drawable.icon_on + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/database/repository/TaskRepository.kt b/app/src/main/java/com/idormy/sms/forwarder/database/repository/TaskRepository.kt new file mode 100644 index 00000000..edbee406 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/database/repository/TaskRepository.kt @@ -0,0 +1,30 @@ +package com.idormy.sms.forwarder.database.repository + +import androidx.annotation.WorkerThread +import androidx.sqlite.db.SimpleSQLiteQuery +import com.idormy.sms.forwarder.database.dao.TaskDao +import com.idormy.sms.forwarder.database.entity.Task + +class TaskRepository(private val taskDao: TaskDao) { + + @WorkerThread + fun insert(task: Task) = taskDao.insert(task) + + fun getOne(id: Long) = taskDao.getOne(id) + + fun update(task: Task) = taskDao.update(task) + + fun getAllNonCache(): List { + val query = SimpleSQLiteQuery("SELECT * FROM Task ORDER BY id ASC") + return taskDao.getAllRaw(query) + } + + @WorkerThread + fun delete(id: Long) { + taskDao.delete(id) + } + + fun deleteAll() { + taskDao.deleteAll() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/database/viewmodel/BaseViewModelFactory.kt b/app/src/main/java/com/idormy/sms/forwarder/database/viewmodel/BaseViewModelFactory.kt index 6f802096..9e0985f9 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/database/viewmodel/BaseViewModelFactory.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/database/viewmodel/BaseViewModelFactory.kt @@ -17,26 +17,36 @@ class BaseViewModelFactory(private val context: Context?) : ViewModelProvider.Fa @Suppress("UNCHECKED_CAST") return FrpcViewModel(frpcDao) as T } + modelClass.isAssignableFrom(MsgViewModel::class.java) -> { val msgDao = AppDatabase.getInstance(context).msgDao() @Suppress("UNCHECKED_CAST") return MsgViewModel(msgDao) as T } + modelClass.isAssignableFrom(LogsViewModel::class.java) -> { val logDao = AppDatabase.getInstance(context).logsDao() @Suppress("UNCHECKED_CAST") return LogsViewModel(logDao) as T } + modelClass.isAssignableFrom(RuleViewModel::class.java) -> { val ruleDao = AppDatabase.getInstance(context).ruleDao() @Suppress("UNCHECKED_CAST") return RuleViewModel(ruleDao) as T } + modelClass.isAssignableFrom(SenderViewModel::class.java) -> { val senderDao = AppDatabase.getInstance(context).senderDao() @Suppress("UNCHECKED_CAST") return SenderViewModel(senderDao) as T } + + modelClass.isAssignableFrom(TaskViewModel::class.java) -> { + val taskDao = AppDatabase.getInstance(context).taskDao() + @Suppress("UNCHECKED_CAST") + return TaskViewModel(taskDao) as T + } } throw IllegalArgumentException("Unknown ViewModel class") diff --git a/app/src/main/java/com/idormy/sms/forwarder/database/viewmodel/TaskViewModel.kt b/app/src/main/java/com/idormy/sms/forwarder/database/viewmodel/TaskViewModel.kt new file mode 100644 index 00000000..3edd8bbc --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/database/viewmodel/TaskViewModel.kt @@ -0,0 +1,39 @@ +package com.idormy.sms.forwarder.database.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.idormy.sms.forwarder.database.dao.TaskDao +import com.idormy.sms.forwarder.database.entity.Task +import com.idormy.sms.forwarder.database.ext.ioThread +import kotlinx.coroutines.flow.Flow + +class TaskViewModel(private val dao: TaskDao) : ViewModel() { + private var type: String = "sms" + + fun setType(type: String): TaskViewModel { + this.type = type + return this + } + + val allTasks: Flow> = Pager( + config = PagingConfig( + pageSize = 10, + enablePlaceholders = false, + initialLoadSize = 10 + ) + ) { + dao.pagingSource(type) + }.flow.cachedIn(viewModelScope) + + fun insertOrUpdate(task: Task) = ioThread { + if (task.id > 0) dao.update(task) else dao.insert(task) + } + + fun delete(id: Long) = ioThread { + dao.delete(id) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/entity/CloneInfo.kt b/app/src/main/java/com/idormy/sms/forwarder/entity/CloneInfo.kt index eb800191..49d03ef1 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/entity/CloneInfo.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/entity/CloneInfo.kt @@ -4,6 +4,7 @@ import com.google.gson.annotations.SerializedName import com.idormy.sms.forwarder.database.entity.Frpc import com.idormy.sms.forwarder.database.entity.Rule import com.idormy.sms.forwarder.database.entity.Sender +import com.idormy.sms.forwarder.database.entity.Task import java.io.Serializable data class CloneInfo( @@ -24,4 +25,7 @@ data class CloneInfo( @SerializedName("frpc_list") var frpcList: List? = null, + + @SerializedName("task_list") + var taskList: List? = null, ) : Serializable \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/entity/task/CronSetting.kt b/app/src/main/java/com/idormy/sms/forwarder/entity/task/CronSetting.kt new file mode 100644 index 00000000..e0b3d06d --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/entity/task/CronSetting.kt @@ -0,0 +1,8 @@ +package com.idormy.sms.forwarder.entity.task + +import java.io.Serializable + +data class CronSetting( + var expression: String, + var description: String = "", +) : Serializable diff --git a/app/src/main/java/com/idormy/sms/forwarder/entity/task/TaskSetting.kt b/app/src/main/java/com/idormy/sms/forwarder/entity/task/TaskSetting.kt new file mode 100644 index 00000000..8266861d --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/entity/task/TaskSetting.kt @@ -0,0 +1,24 @@ +package com.idormy.sms.forwarder.entity.task + +import com.idormy.sms.forwarder.R +import com.idormy.sms.forwarder.utils.TYPE_BARK +import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_GROUP_ROBOT +import com.idormy.sms.forwarder.utils.TYPE_EMAIL +import java.io.Serializable + +data class TaskSetting( + val type: Int, + val title: String, + val description: String, + var setting: String = "", + var position: Int = -1 +) : Serializable { + + val iconId: Int + get() = when (type) { + TYPE_DINGTALK_GROUP_ROBOT -> R.drawable.icon_dingtalk + TYPE_EMAIL -> R.drawable.icon_email + TYPE_BARK -> R.drawable.icon_bark + else -> R.drawable.icon_sms + } +} diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/RulesEditFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/RulesEditFragment.kt index 4aa9f076..7efc3dde 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/fragment/RulesEditFragment.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/RulesEditFragment.kt @@ -458,7 +458,7 @@ class RulesEditFragment : BaseFragment(), View.OnClic } /** - * 动态增删header + * 动态增删Sender * * @param senderItemMap 管理item的map,用于删除指定header * @param layoutSenders 需要挂载item的LinearLayout 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 new file mode 100644 index 00000000..930beda4 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/TasksEditFragment.kt @@ -0,0 +1,352 @@ +package com.idormy.sms.forwarder.fragment + +import android.annotation.SuppressLint +import android.content.Intent +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.gson.Gson +import com.idormy.sms.forwarder.R +import com.idormy.sms.forwarder.adapter.WidgetItemAdapter +import com.idormy.sms.forwarder.adapter.spinner.ActionAdapterItem +import com.idormy.sms.forwarder.adapter.spinner.ConditionAdapterItem +import com.idormy.sms.forwarder.core.BaseFragment +import com.idormy.sms.forwarder.database.AppDatabase +import com.idormy.sms.forwarder.database.entity.Sender +import com.idormy.sms.forwarder.database.entity.Task +import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory +import com.idormy.sms.forwarder.database.viewmodel.TaskViewModel +import com.idormy.sms.forwarder.databinding.FragmentTasksEditBinding +import com.idormy.sms.forwarder.entity.task.CronSetting +import com.idormy.sms.forwarder.entity.task.TaskSetting +import com.idormy.sms.forwarder.utils.* +import com.xuexiang.xaop.annotation.SingleClick +import com.xuexiang.xpage.annotation.Page +import com.xuexiang.xpage.base.XPageFragment +import com.xuexiang.xpage.core.PageOption +import com.xuexiang.xpage.model.PageInfo +import com.xuexiang.xrouter.annotation.AutoWired +import com.xuexiang.xrouter.launcher.XRouter +import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder +import com.xuexiang.xui.utils.DensityUtils +import com.xuexiang.xui.utils.WidgetUtils +import com.xuexiang.xui.widget.actionbar.TitleBar +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.* +import java.util.* + + +@Page(name = "自动任务·编辑器") +@Suppress("PrivatePropertyName", "unused", "DEPRECATION", "UNUSED_PARAMETER") +class TasksEditFragment : BaseFragment(), View.OnClickListener, CompoundButton.OnCheckedChangeListener, RecyclerViewHolder.OnItemClickListener { + + private val TAG: String = TasksEditFragment::class.java.simpleName + private val that = this + var titleBar: TitleBar? = null + private val viewModel by viewModels { BaseViewModelFactory(context) } + private val dialog: BottomSheetDialog by lazy { + BottomSheetDialog(requireContext()) + } + + //触发条件列表 + private var conditionId = 0L + private var conditionListSelected: MutableList = mutableListOf() + private var conditionItemMap = HashMap(2) + + //执行动作列表 + private var actionId = 0L + private var actionListSelected: MutableList = mutableListOf() + private var actionItemMap = HashMap(2) + + @JvmField + @AutoWired(name = KEY_RULE_ID) + var taskId: Long = 0 + + @JvmField + @AutoWired(name = KEY_RULE_TYPE) + var taskType: String = "sms" + + @JvmField + @AutoWired(name = KEY_RULE_CLONE) + var isClone: Boolean = false + + //初始化数据 + private val itemListConditions = mutableListOf( + TaskSetting(TYPE_DINGTALK_GROUP_ROBOT, "Item 1", "Description 1"), TaskSetting(TYPE_EMAIL, "Item 2", "Description 2"), TaskSetting(TYPE_BARK, "Item 3", "Description 3") + // ... other items + ) + private val itemListActions = mutableListOf( + TaskSetting(TYPE_DINGTALK_GROUP_ROBOT, "Apple", "Description Apple"), TaskSetting(TYPE_EMAIL, "Banana", "Description Banana"), TaskSetting(TYPE_BARK, "Orange", "Description Orange") + // ... other items + ) + + override fun initArgs() { + XRouter.getInstance().inject(this) + } + + override fun viewBindingInflate( + inflater: LayoutInflater, + container: ViewGroup, + ): FragmentTasksEditBinding { + return FragmentTasksEditBinding.inflate(inflater, container, false) + } + + override fun initTitle(): TitleBar? { + titleBar = super.initTitle()!!.setImmersive(false) + titleBar!!.setTitle(R.string.menu_tasks) + return titleBar + } + + /** + * 初始化控件 + */ + override fun initViews() { + if (taskId <= 0) { //新增 + titleBar?.setSubTitle(getString(R.string.add_task)) + binding!!.btnDel.setText(R.string.discard) + } else { //编辑 & 克隆 + binding!!.btnDel.setText(R.string.del) + initForm() + } + } + + override fun initListeners() { + binding!!.layoutAddCondition.setOnClickListener(this) + binding!!.layoutAddAction.setOnClickListener(this) + binding!!.btnTest.setOnClickListener(this) + binding!!.btnDel.setOnClickListener(this) + binding!!.btnSave.setOnClickListener(this) + } + + override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {/*when (buttonView?.id) { + }*/ + } + + @SuppressLint("InflateParams") + @SingleClick + override fun onClick(v: View) { + try { + when (v.id) { + R.id.layout_add_condition -> { + val view: View = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_task_condition_bottom_sheet, null) + val recyclerView: RecyclerView = view.findViewById(R.id.recyclerView) + + WidgetUtils.initGridRecyclerView(recyclerView, 4, DensityUtils.dp2px(1f)) + val widgetItemAdapter = WidgetItemAdapter(TASK_CONDITION_FRAGMENT_LIST) + widgetItemAdapter.setOnItemClickListener(that) + recyclerView.adapter = widgetItemAdapter + + dialog.setContentView(view) + dialog.setCancelable(true) + dialog.setCanceledOnTouchOutside(true) + dialog.show() + WidgetUtils.transparentBottomSheetDialogBackground(dialog) + } + + R.id.layout_add_action -> { + val view: View = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_task_action_bottom_sheet, null) + val recyclerView: RecyclerView = view.findViewById(R.id.recyclerView) + + WidgetUtils.initGridRecyclerView(recyclerView, 4, DensityUtils.dp2px(1f)) + val widgetItemAdapter = WidgetItemAdapter(TASK_ACTION_FRAGMENT_LIST) + widgetItemAdapter.setOnItemClickListener(that) + recyclerView.adapter = widgetItemAdapter + + dialog.setContentView(view) + dialog.setCancelable(true) + dialog.setCanceledOnTouchOutside(true) + dialog.show() + WidgetUtils.transparentBottomSheetDialogBackground(dialog) + } + + R.id.btn_test -> { + val taskNew = checkForm() + testTask(taskNew) + return + } + + R.id.btn_del -> { + if (taskId <= 0 || isClone) { + popToBack() + return + } + + //TODO: 删除前确认 + return + } + + R.id.btn_save -> { + val taskNew = checkForm() + if (isClone) taskNew.id = 0 + Log.d(TAG, taskNew.toString()) + viewModel.insertOrUpdate(taskNew) + XToastUtils.success(R.string.tipSaveSuccess) + popToBack() + return + } + } + } catch (e: Exception) { + XToastUtils.error(e.message.toString()) + e.printStackTrace() + } + } + + /** + * 动态增删ConditionItem + * + * @param conditionItemMap 管理item的map,用于删除指定header + * @param layoutConditions 需要挂载item的LinearLayout + * @param condition ConditionAdapterItem + */ + @SuppressLint("SetTextI18n") + private fun addConditionItemLinearLayout( + conditionItemMap: MutableMap, layoutConditions: LinearLayout, condition: ConditionAdapterItem + ) { + val layoutConditionItem = View.inflate(requireContext(), R.layout.item_add_condition, null) as LinearLayout + val ivRemoveCondition = layoutConditionItem.findViewById(R.id.iv_remove_condition) + val ivConditionImage = layoutConditionItem.findViewById(R.id.iv_condition_image) + val tvConditionName = layoutConditionItem.findViewById(R.id.tv_condition_name) + + ivConditionImage.setImageDrawable(condition.icon) + val conditionItemId = condition.id as Long + tvConditionName.text = "ID-$conditionItemId:${condition.title}" + + ivRemoveCondition.tag = conditionItemId + ivRemoveCondition.setOnClickListener { view2: View -> + val tagId = view2.tag as Long + layoutConditions.removeView(conditionItemMap[tagId]) + conditionItemMap.remove(tagId) + } + layoutConditions.addView(layoutConditionItem) + conditionItemMap[conditionItemId] = layoutConditionItem + + if (conditionItemMap.isNotEmpty()) { + binding!!.tvAddCondition.text = getString(R.string.add_condition_continue) + binding!!.tvAddConditionTips.visibility = View.GONE + } else { + binding!!.tvAddCondition.text = getString(R.string.add_condition) + binding!!.tvAddConditionTips.visibility = View.VISIBLE + } + } + + /** + * 动态增删ActionItem + * + * @param actionItemMap 管理item的map,用于删除指定header + * @param layoutActions 需要挂载item的LinearLayout + * @param action ActionAdapterItem + */ + @SuppressLint("SetTextI18n") + private fun addActionItemLinearLayout( + actionItemMap: MutableMap, layoutActions: LinearLayout, action: ActionAdapterItem + ) { + val layoutActionItem = View.inflate(requireContext(), R.layout.item_add_action, null) as LinearLayout + val ivRemoveAction = layoutActionItem.findViewById(R.id.iv_remove_action) + val ivActionImage = layoutActionItem.findViewById(R.id.iv_action_image) + val tvActionName = layoutActionItem.findViewById(R.id.tv_action_name) + + ivActionImage.setImageDrawable(action.icon) + val actionItemId = action.id as Long + tvActionName.text = "ID-$actionItemId:${action.title}" + + ivRemoveAction.tag = actionItemId + ivRemoveAction.setOnClickListener { view2: View -> + val tagId = view2.tag as Long + layoutActions.removeView(actionItemMap[tagId]) + actionItemMap.remove(tagId) + } + layoutActions.addView(layoutActionItem) + actionItemMap[actionItemId] = layoutActionItem + + if (actionItemMap.isNotEmpty()) { + binding!!.tvAddAction.text = getString(R.string.add_action_continue) + binding!!.tvAddActionTips.visibility = View.GONE + } else { + binding!!.tvAddAction.text = getString(R.string.add_action) + binding!!.tvAddActionTips.visibility = View.VISIBLE + } + } + + //初始化表单 + private fun initForm() { + AppDatabase.getInstance(requireContext()).taskDao().get(taskId).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : SingleObserver { + override fun onSubscribe(d: Disposable) {} + + override fun onError(e: Throwable) { + e.printStackTrace() + } + + override fun onSuccess(task: Task) { + Log.d(TAG, task.toString()) + if (isClone) { + titleBar?.setSubTitle(getString(R.string.clone_task)) + binding!!.btnDel.setText(R.string.discard) + } else { + titleBar?.setSubTitle(getString(R.string.edit_task)) + } + binding!!.etName.setText(task.name) + binding!!.sbStatus.isChecked = task.status == STATUS_ON + } + }) + } + + //提交前检查表单 + private fun checkForm(): Task { + if (conditionListSelected.isEmpty() || conditionId == 0L) { + throw Exception(getString(R.string.new_sender_first)) + } + + if (actionListSelected.isEmpty() || actionId == 0L) { + throw Exception(getString(R.string.new_sender_first)) + } + return Task() + } + + private fun testTask(task: Task) { + + } + + @SingleClick + override fun onItemClick(itemView: View, widgetInfo: PageInfo, pos: Int) { + try { + dialog.dismiss() + Log.d(TAG, "onItemClick: $widgetInfo") + @Suppress("UNCHECKED_CAST") PageOption.to(Class.forName(widgetInfo.classPath) as Class) //跳转的fragment + .setRequestCode(pos) //请求码,用于返回结果 + .open(this) + } catch (e: Exception) { + e.printStackTrace() + XToastUtils.error(e.message.toString()) + } + } + + override fun onFragmentResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onFragmentResult(requestCode, resultCode, data) + if (data != null) { + val extras = data.extras + var backData: String? = null + if (resultCode == KEY_BACK_CODE_CONDITION) { + backData = extras!!.getString(KEY_BACK_DATA_CONDITION) + if (backData == null) return + when (requestCode) { + 0 -> { + val settingVo = Gson().fromJson(backData, CronSetting::class.java) + val condition = ConditionAdapterItem(settingVo.expression) //TODO: 构建列表项目 + addConditionItemLinearLayout(conditionItemMap, binding!!.layoutConditions, condition) + } + } + } else if (resultCode == KEY_BACK_CODE_ACTION) { + backData = extras!!.getString(KEY_BACK_DATA_ACTION) + } + Log.d(TAG, "requestCode:$requestCode resultCode:$resultCode backData:$backData") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/TasksFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/TasksFragment.kt new file mode 100644 index 00000000..eec11185 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/TasksFragment.kt @@ -0,0 +1,129 @@ +package com.idormy.sms.forwarder.fragment + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView.RecycledViewPool +import com.alibaba.android.vlayout.VirtualLayoutManager +import com.idormy.sms.forwarder.R +import com.idormy.sms.forwarder.adapter.TaskPagingAdapter +import com.idormy.sms.forwarder.core.BaseFragment +import com.idormy.sms.forwarder.database.entity.Task +import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory +import com.idormy.sms.forwarder.database.viewmodel.TaskViewModel +import com.idormy.sms.forwarder.databinding.FragmentTasksBinding +import com.idormy.sms.forwarder.utils.EVENT_UPDATE_RULE_TYPE +import com.idormy.sms.forwarder.utils.KEY_RULE_CLONE +import com.idormy.sms.forwarder.utils.KEY_RULE_ID +import com.idormy.sms.forwarder.utils.KEY_RULE_TYPE +import com.idormy.sms.forwarder.utils.XToastUtils +import com.jeremyliao.liveeventbus.LiveEventBus +import com.scwang.smartrefresh.layout.api.RefreshLayout +import com.xuexiang.xaop.annotation.SingleClick +import com.xuexiang.xpage.annotation.Page +import com.xuexiang.xpage.core.PageOption +import com.xuexiang.xui.utils.ResUtils +import com.xuexiang.xui.utils.ThemeUtils +import com.xuexiang.xui.widget.actionbar.TitleBar +import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction +import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@Suppress("PropertyName", "DEPRECATION") +@Page(name = "自动任务") +class TasksFragment : BaseFragment(), TaskPagingAdapter.OnItemClickListener { + + val TAG: String = TasksFragment::class.java.simpleName + var titleBar: TitleBar? = null + private var adapter = TaskPagingAdapter(this) + private val viewModel by viewModels { BaseViewModelFactory(context) } + private var currentType: String = "mine" + + override fun viewBindingInflate( + inflater: LayoutInflater, + container: ViewGroup, + ): FragmentTasksBinding { + return FragmentTasksBinding.inflate(inflater, container, false) + } + + override fun initTitle(): TitleBar? { + titleBar = super.initTitle()!!.setImmersive(false) + titleBar!!.setTitle(R.string.menu_tasks) + titleBar!!.setActionTextColor(ThemeUtils.resolveColor(context, R.attr.colorAccent)) + titleBar!!.addAction(object : TitleBar.ImageAction(R.drawable.ic_add) { + @SingleClick + override fun performAction(view: View) { + openNewPage(TasksEditFragment::class.java) + } + }) + return titleBar + } + + /** + * 初始化控件 + */ + override fun initViews() { + val virtualLayoutManager = VirtualLayoutManager(requireContext()) + binding!!.recyclerView.layoutManager = virtualLayoutManager + val viewPool = RecycledViewPool() + binding!!.recyclerView.setRecycledViewPool(viewPool) + viewPool.setMaxRecycledViews(0, 10) + + binding!!.tabBar.setTabTitles(ResUtils.getStringArray(R.array.task_type_option)) + binding!!.tabBar.setOnTabClickListener { _, position -> + //XToastUtils.toast("点击了$title--$position") + currentType = when (position) { + 1 -> "fixed" + else -> "mine" + } + viewModel.setType(currentType) + LiveEventBus.get(EVENT_UPDATE_RULE_TYPE, String::class.java).post(currentType) + adapter.refresh() + binding!!.recyclerView.scrollToPosition(0) + } + } + + override fun initListeners() { + binding!!.recyclerView.adapter = adapter + + //下拉刷新 + binding!!.refreshLayout.setOnRefreshListener { refreshLayout: RefreshLayout -> + refreshLayout.layout.postDelayed({ + //adapter!!.refresh() + lifecycleScope.launch { + viewModel.setType(currentType).allTasks.collectLatest { adapter.submitData(it) } + } + refreshLayout.finishRefresh() + }, 200) + } + + binding!!.refreshLayout.autoRefresh() + } + + override fun onItemClicked(view: View?, item: Task) { + when (view?.id) { + R.id.iv_copy -> { + PageOption.to(TasksEditFragment::class.java).setNewActivity(true).putLong(KEY_RULE_ID, item.id).putString(KEY_RULE_TYPE, item.type.toString()).putBoolean(KEY_RULE_CLONE, true).open(this) + } + + R.id.iv_edit -> { + PageOption.to(TasksEditFragment::class.java).setNewActivity(true).putLong(KEY_RULE_ID, item.id).putString(KEY_RULE_TYPE, item.type.toString()).open(this) + } + + R.id.iv_delete -> { + MaterialDialog.Builder(requireContext()).title(R.string.delete_task_title).content(R.string.delete_task_tips).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? -> + viewModel.delete(item.id) + XToastUtils.success(R.string.delete_task_toast) + }.show() + } + + else -> {} + } + } + + override fun onItemRemove(view: View?, id: Int) {} + +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/CronFragment.kt b/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/CronFragment.kt new file mode 100644 index 00000000..eaa7ba0e --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/fragment/condition/CronFragment.kt @@ -0,0 +1,1650 @@ +package com.idormy.sms.forwarder.fragment.condition + +import android.annotation.SuppressLint +import android.content.Intent +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.RadioGroup +import com.google.gson.Gson +import com.idormy.sms.forwarder.R +import com.idormy.sms.forwarder.core.BaseFragment +import com.idormy.sms.forwarder.databinding.FragmentTasksCronBinding +import com.idormy.sms.forwarder.entity.task.CronSetting +import com.idormy.sms.forwarder.utils.KEY_BACK_CODE_CONDITION +import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_CONDITION +import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_CONDITION +import com.idormy.sms.forwarder.utils.KEY_TEST_CONDITION +import com.idormy.sms.forwarder.utils.XToastUtils +import com.jeremyliao.liveeventbus.LiveEventBus +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.flowlayout.FlowTagLayout +import gatewayapps.crondroid.CronExpression +import net.redhogs.cronparser.CronExpressionDescriptor +import net.redhogs.cronparser.Options +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + + +@Page(name = "Cron") +@Suppress("PrivatePropertyName") +class CronFragment : BaseFragment(), View.OnClickListener { + + private val TAG: String = CronFragment::class.java.simpleName + var titleBar: TitleBar? = null + private var mCountDownHelper: CountDownButtonHelper? = null + + @JvmField + @AutoWired(name = KEY_EVENT_DATA_CONDITION) + var eventData: String? = null + + private val secondsList: List = (0..59).map { String.format("%02d", it) } + private var selectedSecondList = "" + + private val minutesList: List = (0..59).map { String.format("%02d", it) } + private var selectedMinuteList = "" + + private val hoursList: List = (0..23).map { String.format("%02d", it) } + private var selectedHourList = "" + + private val dayList: List = (1..31).map { String.format("%d", it) } + private var selectedDayList = "" + + //private val monthList = listOf("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC") + private val monthList: List = (1..12).map { String.format("%d", it) } + private var selectedMonthList = "" + + //private val weekList = listOf("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN") + private val weekList: List = (1..7).map { String.format("%d", it) } + private var selectedWeekList = "" + + private val yearList: List = (2020..2099).map { String.format("%d", it) } + private var selectedYearList = "" + + private var second = "*" + private var minute = "*" + private var hour = "*" + private var day = "*" + private var month = "*" + private var week = "?" + private var year = "*" + private var expression = "$second $minute $hour $day $month $week $year" + private var description = "" + + override fun initArgs() { + XRouter.getInstance().inject(this) + } + + override fun viewBindingInflate( + inflater: LayoutInflater, + container: ViewGroup, + ): FragmentTasksCronBinding { + return FragmentTasksCronBinding.inflate(inflater, container, false) + } + + override fun initTitle(): TitleBar? { + titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.task_cron) + return titleBar + } + + /** + * 初始化控件 + */ + override fun initViews() { + //测试按钮增加倒计时,避免重复点击 + mCountDownHelper = CountDownButtonHelper(binding!!.btnTest, 3) + 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, CronSetting::class.java) + expression = settingVo.expression + Log.d(TAG, "initViews expression:$expression") + + val fields = expression.split(" ") + second = fields.getOrNull(0) ?: "*" + minute = fields.getOrNull(1) ?: "*" + hour = fields.getOrNull(2) ?: "*" + day = fields.getOrNull(3) ?: "*" + month = fields.getOrNull(4) ?: "*" + week = fields.getOrNull(5) ?: "?" + year = fields.getOrNull(6) ?: "*" + } + + //初始化输入提示 + initSecondInputHelper() + initMinuteInputHelper() + initHourInputHelper() + initDayInputHelper() + initMonthInputHelper() + initWeekInputHelper() + initYearInputHelper() + } + + @SuppressLint("SetTextI18n") + override fun initListeners() { + binding!!.btnTest.setOnClickListener(this) + binding!!.btnDel.setOnClickListener(this) + binding!!.btnSave.setOnClickListener(this) + LiveEventBus.get(KEY_TEST_CONDITION, String::class.java).observe(this) { + mCountDownHelper?.finish() + switchInputHelper(binding!!.layoutCronExpressionCheck) + if (it == "success") { + //生成最近10次运行时间 + val nextTimeList = mutableListOf() + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val cronExpression = CronExpression(expression) + var nextDate = Date() + var times = 0 + for (i in 0 until 10) { + nextDate = cronExpression.getNextValidTimeAfter(nextDate) ?: break + nextTimeList.add(dateFormat.format(nextDate)) + times++ + } + binding!!.tvCronExpressionCheckTips.text = "$expression\n$description" + binding!!.tvNextTimeList.text = String.format(getString(R.string.next_execution_times), times.toString(), nextTimeList.joinToString("\n")) + binding!!.tvNextTimeList.visibility = View.VISIBLE + binding!!.separatorCronExpressionCheck.visibility = View.VISIBLE + } else { + binding!!.tvCronExpressionCheckTips.text = String.format(getString(R.string.invalid_cron_expression), it.toString()) + binding!!.tvNextTimeList.text = "" + binding!!.tvNextTimeList.visibility = View.GONE + binding!!.separatorCronExpressionCheck.visibility = View.GONE + } + } + } + + @SingleClick + override fun onClick(v: View) { + try { + when (v.id) { + R.id.btn_test -> { + mCountDownHelper?.start() + Thread { + try { + val settingVo = checkSetting() + Log.d(TAG, settingVo.toString()) + LiveEventBus.get(KEY_TEST_CONDITION, String::class.java).post("success") + } catch (e: Exception) { + //if (Looper.myLooper() == null) Looper.prepare() + //XToastUtils.error(e.message.toString(), 30000) + //Looper.loop() + LiveEventBus.get(KEY_TEST_CONDITION, String::class.java).post(e.message.toString()) + e.printStackTrace() + } + }.start() + return + } + + R.id.btn_del -> { + popToBack() + return + } + + R.id.btn_save -> { + val settingVo = checkSetting() + val intent = Intent() + intent.putExtra(KEY_BACK_DATA_CONDITION, Gson().toJson(settingVo)) + setFragmentResult(KEY_BACK_CODE_CONDITION, intent) + popToBack() + return + } + } + } catch (e: Exception) { + XToastUtils.error(e.message.toString(), 30000) + e.printStackTrace() + } + } + + //切换输入提示 + private fun switchInputHelper(layout: LinearLayout) { + binding!!.layoutSecondType.visibility = View.GONE + binding!!.layoutMinuteType.visibility = View.GONE + binding!!.layoutHourType.visibility = View.GONE + binding!!.layoutDayType.visibility = View.GONE + binding!!.layoutMonthType.visibility = View.GONE + binding!!.layoutWeekType.visibility = View.GONE + binding!!.layoutYearType.visibility = View.GONE + binding!!.layoutCronExpressionCheck.visibility = View.GONE + layout.visibility = View.VISIBLE + } + + //初始化输入提示--秒 + @SuppressLint("SetTextI18n") + private fun initSecondInputHelper() { + binding!!.etSecond.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + switchInputHelper(binding!!.layoutSecondType) + } else { + afterSecondChanged() + } + } + /*binding!!.etSecond.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + afterSecondChanged() + } + })*/ + binding!!.etSecond.setText(second) + afterSecondChanged() + + //秒类型 + binding!!.rgSecondType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + when (checkedId) { + R.id.rb_second_type_cyclic -> { + var secondCyclicFrom = binding!!.etSecondCyclicFrom.text.toString().trim() + if (secondCyclicFrom.isEmpty()) { + secondCyclicFrom = "00" + binding!!.etSecondCyclicFrom.setText(secondCyclicFrom) + } + var secondCyclicTo = binding!!.etSecondCyclicTo.text.toString().trim() + if (secondCyclicTo.isEmpty()) { + secondCyclicTo = "59" + binding!!.etSecondCyclicTo.setText(secondCyclicTo) + } + second = "$secondCyclicFrom-$secondCyclicTo" + } + + R.id.rb_second_type_interval -> { + var secondIntervalStart = binding!!.etSecondIntervalStart.text.toString().trim() + if (secondIntervalStart.isEmpty()) { + secondIntervalStart = "0" + binding!!.etSecondIntervalStart.setText(secondIntervalStart) + } + var secondInterval = binding!!.etSecondInterval.text.toString().trim() + if (secondInterval.isEmpty()) { + secondInterval = "2" + binding!!.etSecondInterval.setText(secondInterval) + } + second = "$secondIntervalStart/$secondInterval" + } + + R.id.rb_second_type_assigned -> { + if (selectedSecondList.isEmpty()) { + selectedSecondList = "00" + binding!!.flowlayoutMultiSelectSecond.setSelectedItems("00") + } + second = selectedSecondList + } + + else -> { + second = "*" + } + } + binding!!.etSecond.setText(second) + } + + //初始化输入提示--秒--周期 + val secondCyclicWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val secondCyclicFrom = binding!!.etSecondCyclicFrom.text.toString().trim() + val secondCyclicTo = binding!!.etSecondCyclicTo.text.toString().trim() + if (secondCyclicFrom.isNotEmpty() && secondCyclicTo.isNotEmpty()) { + second = "$secondCyclicFrom-$secondCyclicTo" + binding!!.etSecond.setText(second) + binding!!.rbSecondTypeCyclic.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etSecondCyclicFrom.addTextChangedListener(secondCyclicWatcher) + binding!!.etSecondCyclicTo.addTextChangedListener(secondCyclicWatcher) + + //初始化输入提示--秒--间隔 + val secondIntervalWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val secondIntervalStart = binding!!.etSecondIntervalStart.text.toString().trim() + val secondInterval = binding!!.etSecondInterval.text.toString().trim() + if (secondIntervalStart.isNotEmpty() && secondInterval.isNotEmpty()) { + second = "$secondIntervalStart/$secondInterval" + binding!!.etSecond.setText(second) + binding!!.rbSecondTypeInterval.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etSecondIntervalStart.addTextChangedListener(secondIntervalWatcher) + binding!!.etSecondInterval.addTextChangedListener(secondIntervalWatcher) + + //初始化输入提示--秒--指定 + binding!!.flowlayoutMultiSelectSecond.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI) + binding!!.flowlayoutMultiSelectSecond.setItems(secondsList) + binding!!.flowlayoutMultiSelectSecond.setOnTagSelectListener { parent, position, selectedList -> + selectedSecondList = getSelectedItems(parent, selectedList, 1) + Log.d(TAG, "position:$position, selectedSecondList:$selectedSecondList") + if (selectedSecondList.isEmpty()) { + binding!!.rbSecondTypeAll.isChecked = true + second = "*" + } else { + binding!!.rbSecondTypeAssigned.isChecked = true + second = selectedSecondList + } + binding!!.etSecond.setText(second) + } + } + + private fun afterSecondChanged() { + second = binding!!.etSecond.text.toString().trim() + try { + //判断cronExpression是否有效 + expression = "$second $minute $hour $day $month $week $year" + Log.d(TAG, "afterSecondChanged expression:$expression") + CronExpression.validateExpression(expression) + } catch (e: Exception) { + XToastUtils.error("Cron表达式无效:" + e.message, 30000) + return + } + + when { + second == "*" -> { + binding!!.rbSecondTypeAll.isChecked = true + } + + second.contains("/") -> { + val secondsArray = second.split("/") + binding!!.etSecondIntervalStart.setText(secondsArray.getOrNull(0) ?: "0") + binding!!.etSecondInterval.setText(secondsArray.getOrNull(1) ?: "1") + binding!!.rbSecondTypeInterval.isChecked = true + } + + second.contains(",") -> { + val secondsList = restoreMergedItems(second, "%02d") + Log.d(TAG, "secondsList:$secondsList") + binding!!.flowlayoutMultiSelectSecond.setSelectedItems(secondsList) + binding!!.rbSecondTypeAssigned.isChecked = true + selectedSecondList = secondsList.joinToString(",") + } + + second.contains("-") -> { + val secondsArray = second.split("-") + binding!!.etSecondCyclicFrom.setText(secondsArray.getOrNull(0) ?: "00") + binding!!.etSecondCyclicTo.setText(secondsArray.getOrNull(1) ?: "59") + binding!!.rbSecondTypeCyclic.isChecked = true + } + + secondsList.indexOf(String.format("%02d", second.toInt())) != -1 -> { + binding!!.flowlayoutMultiSelectSecond.setSelectedItems(String.format("%02d", second.toInt())) + binding!!.rbSecondTypeAssigned.isChecked = true + selectedSecondList = second + } + + else -> { + binding!!.rbSecondTypeAll.isChecked = true + } + } + } + + //初始化输入提示--分 + @SuppressLint("SetTextI18n") + private fun initMinuteInputHelper() { + binding!!.etMinute.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + switchInputHelper(binding!!.layoutMinuteType) + } else { + afterMinuteChanged() + } + } + /*binding!!.etMinute.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + afterMinuteChanged() + } + })*/ + binding!!.etMinute.setText(minute) + afterMinuteChanged() + + //分类型 + binding!!.rgMinuteType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + when (checkedId) { + R.id.rb_minute_type_cyclic -> { + var minuteCyclicFrom = binding!!.etMinuteCyclicFrom.text.toString().trim() + if (minuteCyclicFrom.isEmpty()) { + minuteCyclicFrom = "00" + binding!!.etMinuteCyclicFrom.setText(minuteCyclicFrom) + } + var minuteCyclicTo = binding!!.etMinuteCyclicTo.text.toString().trim() + if (minuteCyclicTo.isEmpty()) { + minuteCyclicTo = "59" + binding!!.etMinuteCyclicTo.setText(minuteCyclicTo) + } + minute = "$minuteCyclicFrom-$minuteCyclicTo" + } + + R.id.rb_minute_type_interval -> { + var minuteIntervalStart = binding!!.etMinuteIntervalStart.text.toString().trim() + if (minuteIntervalStart.isEmpty()) { + minuteIntervalStart = "0" + binding!!.etMinuteIntervalStart.setText(minuteIntervalStart) + } + var minuteInterval = binding!!.etMinuteInterval.text.toString().trim() + if (minuteInterval.isEmpty()) { + minuteInterval = "2" + binding!!.etMinuteInterval.setText(minuteInterval) + } + minute = "$minuteIntervalStart/$minuteInterval" + } + + R.id.rb_minute_type_assigned -> { + if (selectedMinuteList.isEmpty()) { + selectedMinuteList = "00" + binding!!.flowlayoutMultiSelectMinute.setSelectedItems("00") + } + minute = selectedMinuteList + } + + else -> { + minute = "*" + } + } + binding!!.etMinute.setText(minute) + } + + //初始化输入提示--分--周期 + val minuteCyclicWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val minuteCyclicFrom = binding!!.etMinuteCyclicFrom.text.toString().trim() + val minuteCyclicTo = binding!!.etMinuteCyclicTo.text.toString().trim() + if (minuteCyclicFrom.isNotEmpty() && minuteCyclicTo.isNotEmpty()) { + minute = "$minuteCyclicFrom-$minuteCyclicTo" + binding!!.etMinute.setText(minute) + binding!!.rbMinuteTypeCyclic.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etMinuteCyclicFrom.addTextChangedListener(minuteCyclicWatcher) + binding!!.etMinuteCyclicTo.addTextChangedListener(minuteCyclicWatcher) + + //初始化输入提示--分--间隔 + val minuteIntervalWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val minuteIntervalStart = binding!!.etMinuteIntervalStart.text.toString().trim() + val minuteInterval = binding!!.etMinuteInterval.text.toString().trim() + if (minuteIntervalStart.isNotEmpty() && minuteInterval.isNotEmpty()) { + minute = "$minuteIntervalStart/$minuteInterval" + binding!!.etMinute.setText(minute) + binding!!.rbMinuteTypeInterval.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etMinuteIntervalStart.addTextChangedListener(minuteIntervalWatcher) + binding!!.etMinuteInterval.addTextChangedListener(minuteIntervalWatcher) + + //初始化输入提示--分--指定 + binding!!.flowlayoutMultiSelectMinute.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI) + binding!!.flowlayoutMultiSelectMinute.setItems(minutesList) + binding!!.flowlayoutMultiSelectMinute.setOnTagSelectListener { parent, position, selectedList -> + selectedMinuteList = getSelectedItems(parent, selectedList, 1) + Log.d(TAG, "position:$position, selectedMinutesList:$selectedMinuteList") + if (selectedMinuteList.isEmpty()) { + binding!!.rbMinuteTypeAll.isChecked = true + minute = "*" + } else { + binding!!.rbMinuteTypeAssigned.isChecked = true + minute = selectedMinuteList + } + binding!!.etMinute.setText(minute) + } + } + + private fun afterMinuteChanged() { + minute = binding!!.etMinute.text.toString().trim() + try { + //判断cronExpression是否有效 + expression = "$second $minute $hour $day $month $week $year" + Log.d(TAG, "afterMinuteChanged expression:$expression") + CronExpression.validateExpression(expression) + } catch (e: Exception) { + XToastUtils.error("Cron表达式无效:" + e.message, 30000) + return + } + + when { + minute == "*" -> { + binding!!.rbMinuteTypeAll.isChecked = true + } + + minute.contains("/") -> { + val minutesArray = minute.split("/") + binding!!.etMinuteIntervalStart.setText(minutesArray.getOrNull(0) ?: "0") + binding!!.etMinuteInterval.setText(minutesArray.getOrNull(1) ?: "1") + binding!!.rbMinuteTypeInterval.isChecked = true + } + + minute.contains(",") -> { + val minutesList = restoreMergedItems(minute, "%02d") + Log.d(TAG, "minutesList:$minutesList") + binding!!.flowlayoutMultiSelectMinute.setSelectedItems(minutesList) + binding!!.rbMinuteTypeAssigned.isChecked = true + selectedMinuteList = minutesList.joinToString(",") + } + + minute.contains("-") -> { + val minutesArray = minute.split("-") + binding!!.etMinuteCyclicFrom.setText(minutesArray.getOrNull(0) ?: "00") + binding!!.etMinuteCyclicTo.setText(minutesArray.getOrNull(1) ?: "59") + binding!!.rbMinuteTypeCyclic.isChecked = true + } + + minutesList.indexOf(String.format("%02d", minute.toInt())) != -1 -> { + binding!!.flowlayoutMultiSelectMinute.setSelectedItems(String.format("%02d", minute.toInt())) + binding!!.rbMinuteTypeAssigned.isChecked = true + selectedMinuteList = minute + } + + else -> { + binding!!.rbMinuteTypeAll.isChecked = true + } + } + } + + //初始化输入提示--时 + @SuppressLint("SetTextI18n") + private fun initHourInputHelper() { + binding!!.etHour.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + switchInputHelper(binding!!.layoutHourType) + } else { + afterHourChanged() + } + } + /*binding!!.etHour.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + afterHourChanged() + } + })*/ + binding!!.etHour.setText(hour) + afterHourChanged() + + //时类型 + binding!!.rgHourType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + when (checkedId) { + R.id.rb_hour_type_cyclic -> { + var hourCyclicFrom = binding!!.etHourCyclicFrom.text.toString().trim() + if (hourCyclicFrom.isEmpty()) { + hourCyclicFrom = "00" + binding!!.etHourCyclicFrom.setText(hourCyclicFrom) + } + var hourCyclicTo = binding!!.etHourCyclicTo.text.toString().trim() + if (hourCyclicTo.isEmpty()) { + hourCyclicTo = "23" + binding!!.etHourCyclicTo.setText(hourCyclicTo) + } + hour = "$hourCyclicFrom-$hourCyclicTo" + } + + R.id.rb_hour_type_interval -> { + var hourIntervalStart = binding!!.etHourIntervalStart.text.toString().trim() + if (hourIntervalStart.isEmpty()) { + hourIntervalStart = "0" + binding!!.etHourIntervalStart.setText(hourIntervalStart) + } + var hourInterval = binding!!.etHourInterval.text.toString().trim() + if (hourInterval.isEmpty()) { + hourInterval = "2" + binding!!.etHourInterval.setText(hourInterval) + } + hour = "$hourIntervalStart/$hourInterval" + } + + R.id.rb_hour_type_assigned -> { + if (selectedHourList.isEmpty()) { + selectedHourList = "00" + binding!!.flowlayoutMultiSelectHour.setSelectedItems("00") + } + hour = selectedHourList + } + + else -> { + hour = "*" + } + } + binding!!.etHour.setText(hour) + } + + //初始化输入提示--时--周期 + val hourCyclicWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val hourCyclicFrom = binding!!.etHourCyclicFrom.text.toString().trim() + val hourCyclicTo = binding!!.etHourCyclicTo.text.toString().trim() + if (hourCyclicFrom.isNotEmpty() && hourCyclicTo.isNotEmpty()) { + hour = "$hourCyclicFrom-$hourCyclicTo" + binding!!.etHour.setText(hour) + binding!!.rbHourTypeCyclic.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etHourCyclicFrom.addTextChangedListener(hourCyclicWatcher) + binding!!.etHourCyclicTo.addTextChangedListener(hourCyclicWatcher) + + //初始化输入提示--时--间隔 + val hourIntervalWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val hourIntervalStart = binding!!.etHourIntervalStart.text.toString().trim() + val hourInterval = binding!!.etHourInterval.text.toString().trim() + if (hourIntervalStart.isNotEmpty() && hourInterval.isNotEmpty()) { + hour = "$hourIntervalStart/$hourInterval" + binding!!.etHour.setText(hour) + binding!!.rbHourTypeInterval.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etHourIntervalStart.addTextChangedListener(hourIntervalWatcher) + binding!!.etHourInterval.addTextChangedListener(hourIntervalWatcher) + + //初始化输入提示--时--指定 + binding!!.flowlayoutMultiSelectHour.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI) + binding!!.flowlayoutMultiSelectHour.setItems(hoursList) + binding!!.flowlayoutMultiSelectHour.setOnTagSelectListener { parent, position, selectedList -> + selectedHourList = getSelectedItems(parent, selectedList, 1) + Log.d(TAG, "position:$position, selectedHoursList:$selectedHourList") + if (selectedHourList.isEmpty()) { + binding!!.rbHourTypeAll.isChecked = true + hour = "*" + } else { + binding!!.rbHourTypeAssigned.isChecked = true + hour = selectedHourList + } + binding!!.etHour.setText(hour) + } + } + + private fun afterHourChanged() { + hour = binding!!.etHour.text.toString().trim() + try { + //判断cronExpression是否有效 + expression = "$second $minute $hour $day $month $week $year" + Log.d(TAG, "afterHourChanged expression:$expression") + CronExpression.validateExpression(expression) + } catch (e: Exception) { + XToastUtils.error("Cron表达式无效:" + e.message, 30000) + return + } + + when { + hour == "*" -> { + binding!!.rbHourTypeAll.isChecked = true + } + + hour.contains("/") -> { + val hoursArray = hour.split("/") + binding!!.etHourIntervalStart.setText(hoursArray.getOrNull(0) ?: "0") + binding!!.etHourInterval.setText(hoursArray.getOrNull(1) ?: "1") + binding!!.rbHourTypeInterval.isChecked = true + } + + hour.contains(",") -> { + val hoursList = restoreMergedItems(hour, "%02d") + Log.d(TAG, "hoursList:$hoursList") + binding!!.flowlayoutMultiSelectHour.setSelectedItems(hoursList) + binding!!.rbHourTypeAssigned.isChecked = true + selectedHourList = hoursList.joinToString(",") + } + + hour.contains("-") -> { + val hoursArray = hour.split("-") + binding!!.etHourCyclicFrom.setText(hoursArray.getOrNull(0) ?: "00") + binding!!.etHourCyclicTo.setText(hoursArray.getOrNull(1) ?: "23") + binding!!.rbHourTypeCyclic.isChecked = true + } + + hoursList.indexOf(String.format("%02d", hour.toInt())) != -1 -> { + binding!!.flowlayoutMultiSelectHour.setSelectedItems(String.format("%02d", hour.toInt())) + binding!!.rbHourTypeAssigned.isChecked = true + selectedHourList = hour + } + + else -> { + binding!!.rbHourTypeAll.isChecked = true + } + } + } + + //初始化输入提示--日 + @SuppressLint("SetTextI18n") + private fun initDayInputHelper() { + binding!!.etDay.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + switchInputHelper(binding!!.layoutDayType) + } else { + afterDayChanged() + } + } + /*binding!!.etDay.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + afterDayChanged() + } + })*/ + binding!!.etDay.setText(day) + afterDayChanged() + + //日类型 + binding!!.rgDayType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + when (checkedId) { + R.id.rb_day_type_cyclic -> { + var dayCyclicFrom = binding!!.etDayCyclicFrom.text.toString().trim() + if (dayCyclicFrom.isEmpty()) { + dayCyclicFrom = "1" + binding!!.etDayCyclicFrom.setText(dayCyclicFrom) + } + var dayCyclicTo = binding!!.etDayCyclicTo.text.toString().trim() + if (dayCyclicTo.isEmpty()) { + dayCyclicTo = "31" + binding!!.etDayCyclicTo.setText(dayCyclicTo) + } + day = "$dayCyclicFrom-$dayCyclicTo" + } + + R.id.rb_day_type_interval -> { + var dayIntervalStart = binding!!.etDayIntervalStart.text.toString().trim() + if (dayIntervalStart.isEmpty()) { + dayIntervalStart = "1" + binding!!.etDayIntervalStart.setText(dayIntervalStart) + } + var dayInterval = binding!!.etDayInterval.text.toString().trim() + if (dayInterval.isEmpty()) { + dayInterval = "2" + binding!!.etDayInterval.setText(dayInterval) + } + day = "$dayIntervalStart/$dayInterval" + } + + R.id.rb_day_type_assigned -> { + if (selectedDayList.isEmpty()) { + selectedDayList = "1" + binding!!.flowlayoutMultiSelectDay.setSelectedItems("1") + } + day = selectedDayList + } + + R.id.rb_day_type_last_day_of_month -> { + day = "L" + } + + R.id.rb_day_type_last_day_of_month_recent_day -> { + day = "LW" + } + + R.id.rb_day_type_recent_day -> { + var recentDay = binding!!.etRecentDay.text.toString().trim() + if (recentDay.isEmpty()) { + recentDay = "1" + binding!!.etRecentDay.setText(recentDay) + } + day = recentDay + "W" + } + + R.id.rb_day_type_not_assigned -> { + day = "?" + } + + else -> { + day = "*" + } + } + binding!!.etDay.setText(day) + } + + //初始化输入提示--日--周期 + val dayCyclicWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val dayCyclicFrom = binding!!.etDayCyclicFrom.text.toString().trim() + val dayCyclicTo = binding!!.etDayCyclicTo.text.toString().trim() + if (dayCyclicFrom.isNotEmpty() && dayCyclicTo.isNotEmpty()) { + day = "$dayCyclicFrom-$dayCyclicTo" + binding!!.etDay.setText(day) + binding!!.rbDayTypeCyclic.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etDayCyclicFrom.addTextChangedListener(dayCyclicWatcher) + binding!!.etDayCyclicTo.addTextChangedListener(dayCyclicWatcher) + + //初始化输入提示--日--间隔 + val dayIntervalWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val dayIntervalStart = binding!!.etDayIntervalStart.text.toString().trim() + val dayInterval = binding!!.etDayInterval.text.toString().trim() + if (dayIntervalStart.isNotEmpty() && dayInterval.isNotEmpty()) { + day = "$dayIntervalStart/$dayInterval" + binding!!.etDay.setText(day) + binding!!.rbDayTypeInterval.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etDayIntervalStart.addTextChangedListener(dayIntervalWatcher) + binding!!.etDayInterval.addTextChangedListener(dayIntervalWatcher) + + //初始化输入提示--日--指定 + binding!!.flowlayoutMultiSelectDay.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI) + binding!!.flowlayoutMultiSelectDay.setItems(dayList) + binding!!.flowlayoutMultiSelectDay.setOnTagSelectListener { parent, position, selectedList -> + selectedDayList = getSelectedItems(parent, selectedList) + Log.d(TAG, "position:$position, selectedDayList:$selectedDayList") + if (selectedDayList.isEmpty()) { + binding!!.rbDayTypeAll.isChecked = true + day = "*" + } else { + binding!!.rbDayTypeAssigned.isChecked = true + day = selectedDayList + } + binding!!.etDay.setText(day) + } + } + + private fun afterDayChanged() { + //周和日不能同时设置 + day = binding!!.etDay.text.toString().trim() + if (day != "?" && week != "?") { + week = "?" + binding!!.etWeek.setText(week) + } + + try { + //判断cronExpression是否有效 + expression = "$second $minute $hour $day $month $week $year" + Log.d(TAG, "afterDayChanged expression:$expression") + CronExpression.validateExpression(expression) + } catch (e: Exception) { + XToastUtils.error("Cron表达式无效:" + e.message, 30000) + return + } + + when { + day == "*" -> { + binding!!.rbDayTypeAll.isChecked = true + } + + day == "?" -> { + binding!!.rbDayTypeNotAssigned.isChecked = true + } + + day == "L" -> { + binding!!.rbDayTypeLastDayOfMonth.isChecked = true + } + + day == "LW" -> { + binding!!.rbDayTypeLastDayOfMonthRecentDay.isChecked = true + return + } + + day.endsWith("W") -> { + binding!!.rbDayTypeRecentDay.isChecked = true + binding!!.etRecentDay.setText(day.removeSuffix("W")) + return + } + + day.contains("/") -> { + val dayArray = day.split("/") + binding!!.etDayIntervalStart.setText(dayArray.getOrNull(0) ?: "0") + binding!!.etDayInterval.setText(dayArray.getOrNull(1) ?: "1") + binding!!.rbDayTypeInterval.isChecked = true + } + + day.contains(",") -> { + val dayList = restoreMergedItems(day, "%d") + Log.d(TAG, "dayList:$dayList") + binding!!.flowlayoutMultiSelectDay.setSelectedItems(dayList) + binding!!.rbDayTypeAssigned.isChecked = true + selectedDayList = dayList.joinToString(",") + } + + day.contains("-") -> { + val dayArray = day.split("-") + binding!!.etDayCyclicFrom.setText(dayArray.getOrNull(0) ?: "1") + binding!!.etDayCyclicTo.setText(dayArray.getOrNull(1) ?: "31") + binding!!.rbDayTypeCyclic.isChecked = true + } + + dayList.indexOf(String.format("%d", day.toInt())) != -1 -> { + binding!!.flowlayoutMultiSelectDay.setSelectedItems(String.format("%d", day.toInt())) + binding!!.rbDayTypeAssigned.isChecked = true + selectedDayList = day + } + + else -> { + binding!!.rbDayTypeAll.isChecked = true + } + } + } + + //初始化输入提示--月 + @SuppressLint("SetTextI18n") + private fun initMonthInputHelper() { + binding!!.etMonth.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + switchInputHelper(binding!!.layoutMonthType) + } else { + afterMonthChanged() + } + } + /*binding!!.etMonth.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + afterMonthChanged() + } + })*/ + binding!!.etMonth.setText(month) + afterMonthChanged() + + //月类型 + binding!!.rgMonthType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + when (checkedId) { + R.id.rb_month_type_cyclic -> { + var monthCyclicFrom = binding!!.etMonthCyclicFrom.text.toString().trim() + if (monthCyclicFrom.isEmpty()) { + monthCyclicFrom = "1" + binding!!.etMonthCyclicFrom.setText(monthCyclicFrom) + } + var monthCyclicTo = binding!!.etMonthCyclicTo.text.toString().trim() + if (monthCyclicTo.isEmpty()) { + monthCyclicTo = "12" + binding!!.etMonthCyclicTo.setText(monthCyclicTo) + } + month = "$monthCyclicFrom-$monthCyclicTo" + } + + R.id.rb_month_type_interval -> { + var monthIntervalStart = binding!!.etMonthIntervalStart.text.toString().trim() + if (monthIntervalStart.isEmpty()) { + monthIntervalStart = "1" + binding!!.etMonthIntervalStart.setText(monthIntervalStart) + } + var monthInterval = binding!!.etMonthInterval.text.toString().trim() + if (monthInterval.isEmpty()) { + monthInterval = "2" + binding!!.etMonthInterval.setText(monthInterval) + } + month = "$monthIntervalStart/$monthInterval" + } + + R.id.rb_month_type_assigned -> { + if (selectedMonthList.isEmpty()) { + selectedMonthList = "1" + binding!!.flowlayoutMultiSelectMonth.setSelectedItems("1") + } + month = selectedMonthList + } + + else -> { + month = "*" + } + } + binding!!.etMonth.setText(month) + } + + //初始化输入提示--月--周期 + val monthCyclicWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val monthCyclicFrom = binding!!.etMonthCyclicFrom.text.toString().trim() + val monthCyclicTo = binding!!.etMonthCyclicTo.text.toString().trim() + if (monthCyclicFrom.isNotEmpty() && monthCyclicTo.isNotEmpty()) { + month = "$monthCyclicFrom-$monthCyclicTo" + binding!!.etMonth.setText(month) + binding!!.rbMonthTypeCyclic.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etMonthCyclicFrom.addTextChangedListener(monthCyclicWatcher) + binding!!.etMonthCyclicTo.addTextChangedListener(monthCyclicWatcher) + + //初始化输入提示--月--间隔 + val monthIntervalWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val monthIntervalStart = binding!!.etMonthIntervalStart.text.toString().trim() + val monthInterval = binding!!.etMonthInterval.text.toString().trim() + if (monthIntervalStart.isNotEmpty() && monthInterval.isNotEmpty()) { + month = "$monthIntervalStart/$monthInterval" + binding!!.etMonth.setText(month) + binding!!.rbMonthTypeInterval.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etMonthIntervalStart.addTextChangedListener(monthIntervalWatcher) + binding!!.etMonthInterval.addTextChangedListener(monthIntervalWatcher) + + //初始化输入提示--月--指定 + binding!!.flowlayoutMultiSelectMonth.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI) + binding!!.flowlayoutMultiSelectMonth.setItems(monthList) + binding!!.flowlayoutMultiSelectMonth.setOnTagSelectListener { parent, position, selectedList -> + selectedMonthList = getSelectedItems(parent, selectedList) + Log.d(TAG, "position:$position, selectedMonthList:$selectedMonthList") + if (selectedMonthList.isEmpty()) { + binding!!.rbMonthTypeAll.isChecked = true + month = "*" + } else { + binding!!.rbMonthTypeAssigned.isChecked = true + month = selectedMonthList + } + binding!!.etMonth.setText(month) + } + } + + private fun afterMonthChanged() { + month = binding!!.etMonth.text.toString().trim() + try { + //判断cronExpression是否有效 + expression = "$second $minute $hour $day $month $week $year" + Log.d(TAG, "afterMonthChanged expression:$expression") + CronExpression.validateExpression(expression) + } catch (e: Exception) { + XToastUtils.error("Cron表达式无效:" + e.message, 30000) + return + } + + when { + month == "*" -> { + binding!!.rbMonthTypeAll.isChecked = true + } + + month.contains("/") -> { + val monthArray = month.split("/") + binding!!.etMonthIntervalStart.setText(monthArray.getOrNull(0) ?: "0") + binding!!.etMonthInterval.setText(monthArray.getOrNull(1) ?: "1") + binding!!.rbMonthTypeInterval.isChecked = true + } + + month.contains(",") -> { + val monthList = restoreMergedItems(month, "%d") + Log.d(TAG, "monthList:$monthList") + binding!!.flowlayoutMultiSelectMonth.setSelectedItems(monthList) + binding!!.rbMonthTypeAssigned.isChecked = true + selectedMonthList = monthList.joinToString(",") + } + + month.contains("-") -> { + val monthArray = month.split("-") + binding!!.etMonthCyclicFrom.setText(monthArray.getOrNull(0) ?: "1") + binding!!.etMonthCyclicTo.setText(monthArray.getOrNull(1) ?: "31") + binding!!.rbMonthTypeCyclic.isChecked = true + } + + monthList.indexOf(month) != -1 -> { + binding!!.flowlayoutMultiSelectMonth.setSelectedItems(month) + binding!!.rbMonthTypeAssigned.isChecked = true + selectedMonthList = month + } + + else -> { + binding!!.rbMonthTypeAll.isChecked = true + } + } + } + + //初始化输入提示--周 + private fun initWeekInputHelper() { + binding!!.etWeek.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + switchInputHelper(binding!!.layoutWeekType) + } else { + afterWeekChanged() + } + } + /*binding!!.etWeek.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + afterWeekChanged() + } + })*/ + binding!!.etWeek.setText(week) + afterWeekChanged() + + //周类型 + binding!!.rgWeekType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + when (checkedId) { + R.id.rb_week_type_cyclic -> { + var weekCyclicFrom = binding!!.etWeekCyclicFrom.text.toString().trim() + if (weekCyclicFrom.isEmpty()) { + weekCyclicFrom = "1" + binding!!.etWeekCyclicFrom.setText(weekCyclicFrom) + } + var weekCyclicTo = binding!!.etWeekCyclicTo.text.toString().trim() + if (weekCyclicTo.isEmpty()) { + weekCyclicTo = "7" + binding!!.etWeekCyclicTo.setText(weekCyclicTo) + } + week = "$weekCyclicFrom-$weekCyclicTo" + } + + R.id.rb_week_type_weeks_of_week -> { + var whichWeekOfMonth = binding!!.etWhichWeekOfMonth.text.toString().trim() + if (whichWeekOfMonth.isEmpty()) { + whichWeekOfMonth = "1" + binding!!.etWhichWeekOfMonth.setText(whichWeekOfMonth) + } + var whichDayOfWeek = binding!!.etWhichDayOfWeek.text.toString().trim() + if (whichDayOfWeek.isEmpty()) { + whichDayOfWeek = "1" + binding!!.etWhichDayOfWeek.setText(whichDayOfWeek) + } + week = "$whichWeekOfMonth#$whichDayOfWeek" + } + + R.id.rb_week_type_assigned -> { + if (selectedWeekList.isEmpty()) { + selectedWeekList = "1" + binding!!.flowlayoutMultiSelectWeek.setSelectedItems("1") + } + week = selectedWeekList + } + + R.id.rb_week_type_last_week_of_month -> { + var lastWeekOfMonth = binding!!.etLastWeekOfMonth.text.toString().trim() + if (lastWeekOfMonth.isEmpty()) { + lastWeekOfMonth = "1" + binding!!.etLastWeekOfMonth.setText(lastWeekOfMonth) + } + week = lastWeekOfMonth + "L" + } + + R.id.rb_week_type_all -> { + week = "*" + } + + else -> { + week = "?" + } + } + binding!!.etWeek.setText(week) + } + + //初始化输入提示--周--周期 + val weekCyclicWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val weekCyclicFrom = binding!!.etWeekCyclicFrom.text.toString().trim() + val weekCyclicTo = binding!!.etWeekCyclicTo.text.toString().trim() + if (weekCyclicFrom.isNotEmpty() && weekCyclicTo.isNotEmpty()) { + week = "$weekCyclicFrom-$weekCyclicTo" + binding!!.etWeek.setText(week) + binding!!.rbWeekTypeCyclic.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etWeekCyclicFrom.addTextChangedListener(weekCyclicWatcher) + binding!!.etWeekCyclicTo.addTextChangedListener(weekCyclicWatcher) + + //初始化输入提示--周--间隔 + val weeksOfWeekWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val whichWeekOfMonth = binding!!.etWhichWeekOfMonth.text.toString().trim() + val whichDayOfWeek = binding!!.etWhichDayOfWeek.text.toString().trim() + if (whichWeekOfMonth.isNotEmpty() && whichDayOfWeek.isNotEmpty()) { + week = "$whichWeekOfMonth#$whichDayOfWeek" + binding!!.etWeek.setText(week) + binding!!.rbWeekTypeWeeksOfWeek.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etWhichWeekOfMonth.addTextChangedListener(weeksOfWeekWatcher) + binding!!.etWhichDayOfWeek.addTextChangedListener(weeksOfWeekWatcher) + + //初始化输入提示--周--指定 + binding!!.flowlayoutMultiSelectWeek.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI) + binding!!.flowlayoutMultiSelectWeek.setItems(weekList) + binding!!.flowlayoutMultiSelectWeek.setOnTagSelectListener { parent, position, selectedList -> + selectedWeekList = getSelectedItems(parent, selectedList) + Log.d(TAG, "position:$position, selectedWeekList:$selectedWeekList") + if (selectedWeekList.isEmpty()) { + binding!!.rbWeekTypeAll.isChecked = true + week = "?" + } else { + binding!!.rbWeekTypeAssigned.isChecked = true + week = selectedWeekList + } + binding!!.etWeek.setText(week) + } + + //初始化输入提示--周--本月最后 + binding!!.etLastWeekOfMonth.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val lastWeekOfMonth = binding!!.etLastWeekOfMonth.text.toString().trim() + if (lastWeekOfMonth.isNotEmpty()) { + week = lastWeekOfMonth + "L" + binding!!.etWeek.setText(week) + binding!!.rbWeekTypeLastWeekOfMonth.isChecked = true + } else { + week = "*" + binding!!.rbWeekTypeAll.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + }) + } + + private fun afterWeekChanged() { + //周和日不能同时设置 + week = binding!!.etWeek.text.toString().trim() + if (day != "?" && week != "?") { + day = "?" + binding!!.etDay.setText(day) + } + + try { + //判断cronExpression是否有效 + expression = "$second $minute $hour $day $month $week $year" + Log.d(TAG, "afterWeekChanged expression:$expression") + CronExpression.validateExpression(expression) + } catch (e: Exception) { + XToastUtils.error("Cron表达式无效:" + e.message, 30000) + return + } + + when { + week == "*" -> { + binding!!.rbWeekTypeAll.isChecked = true + } + + week == "?" -> { + binding!!.rbWeekTypeNotAssigned.isChecked = true + } + + week.contains(",") -> { + val weekList = restoreMergedItems(week, "%d") + Log.d(TAG, "weekList:$weekList") + binding!!.flowlayoutMultiSelectWeek.setSelectedItems(weekList) + binding!!.rbWeekTypeAssigned.isChecked = true + selectedWeekList = weekList.joinToString(",") + } + + week.contains("-") -> { + val weekArray = week.split("-") + binding!!.etWeekCyclicFrom.setText(weekArray.getOrNull(0) ?: "1") + binding!!.etWeekCyclicTo.setText(weekArray.getOrNull(1) ?: "31") + binding!!.rbWeekTypeCyclic.isChecked = true + } + + week.contains("#") -> { + val weekArray = week.split("#") + binding!!.etWhichWeekOfMonth.setText(weekArray.getOrNull(0) ?: "1") + binding!!.etWhichDayOfWeek.setText(weekArray.getOrNull(1) ?: "1") + binding!!.rbWeekTypeWeeksOfWeek.isChecked = true + } + + weekList.indexOf(week) != -1 -> { + binding!!.flowlayoutMultiSelectWeek.setSelectedItems(week) + binding!!.rbWeekTypeAssigned.isChecked = true + selectedWeekList = week + } + + week.endsWith("L") -> { + binding!!.rbWeekTypeLastWeekOfMonth.isChecked = true + binding!!.etLastWeekOfMonth.setText(week.removeSuffix("L")) + } + + else -> { + binding!!.rbWeekTypeAll.isChecked = true + } + } + } + + //初始化输入提示--年 + @SuppressLint("SetTextI18n") + private fun initYearInputHelper() { + binding!!.etYear.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + switchInputHelper(binding!!.layoutYearType) + } else { + afterYearChanged() + } + } + /*binding!!.etYear.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + afterYearChanged() + } + })*/ + binding!!.etYear.setText(year) + afterYearChanged() + + //年类型 + binding!!.rgYearType.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> + when (checkedId) { + R.id.rb_year_type_cyclic -> { + var yearCyclicFrom = binding!!.etYearCyclicFrom.text.toString().trim() + if (yearCyclicFrom.isEmpty()) { + yearCyclicFrom = "2023" + binding!!.etYearCyclicFrom.setText(yearCyclicFrom) + } + var yearCyclicTo = binding!!.etYearCyclicTo.text.toString().trim() + if (yearCyclicTo.isEmpty()) { + yearCyclicTo = "2058" + binding!!.etYearCyclicTo.setText(yearCyclicTo) + } + year = "$yearCyclicFrom-$yearCyclicTo" + } + + R.id.rb_year_type_interval -> { + var yearIntervalStart = binding!!.etYearIntervalStart.text.toString().trim() + if (yearIntervalStart.isEmpty()) { + yearIntervalStart = "2023" + binding!!.etYearIntervalStart.setText(yearIntervalStart) + } + var yearInterval = binding!!.etYearInterval.text.toString().trim() + if (yearInterval.isEmpty()) { + yearInterval = "2" + binding!!.etYearInterval.setText(yearInterval) + } + year = "$yearIntervalStart/$yearInterval" + } + + R.id.rb_year_type_assigned -> { + if (selectedYearList.isEmpty()) { + selectedYearList = "1" + binding!!.flowlayoutMultiSelectYear.setSelectedItems("1") + } + year = selectedYearList + } + + else -> { + year = "*" + } + } + binding!!.etYear.setText(year) + } + + //初始化输入提示--年--周期 + val yearCyclicWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val yearCyclicFrom = binding!!.etYearCyclicFrom.text.toString().trim() + val yearCyclicTo = binding!!.etYearCyclicTo.text.toString().trim() + if (yearCyclicFrom.isNotEmpty() && yearCyclicTo.isNotEmpty()) { + year = "$yearCyclicFrom-$yearCyclicTo" + binding!!.etYear.setText(year) + binding!!.rbYearTypeCyclic.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etYearCyclicFrom.addTextChangedListener(yearCyclicWatcher) + binding!!.etYearCyclicTo.addTextChangedListener(yearCyclicWatcher) + + //初始化输入提示--年--间隔 + val yearIntervalWatcher = object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val yearIntervalStart = binding!!.etYearIntervalStart.text.toString().trim() + val yearInterval = binding!!.etYearInterval.text.toString().trim() + if (yearIntervalStart.isNotEmpty() && yearInterval.isNotEmpty()) { + year = "$yearIntervalStart/$yearInterval" + binding!!.etYear.setText(year) + binding!!.rbYearTypeInterval.isChecked = true + } + } + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + } + binding!!.etYearIntervalStart.addTextChangedListener(yearIntervalWatcher) + binding!!.etYearInterval.addTextChangedListener(yearIntervalWatcher) + + //初始化输入提示--年--指定 + binding!!.flowlayoutMultiSelectYear.setTagCheckedMode(FlowTagLayout.FLOW_TAG_CHECKED_MULTI) + binding!!.flowlayoutMultiSelectYear.setItems(yearList) + binding!!.flowlayoutMultiSelectYear.setOnTagSelectListener { parent, position, selectedList -> + selectedYearList = getSelectedItems(parent, selectedList) + Log.d(TAG, "position:$position, selectedYearList:$selectedYearList") + if (selectedYearList.isEmpty()) { + binding!!.rbYearTypeAll.isChecked = true + year = "*" + } else { + binding!!.rbYearTypeAssigned.isChecked = true + year = selectedYearList + } + binding!!.etYear.setText(year) + } + } + + private fun afterYearChanged() { + year = binding!!.etYear.text.toString().trim() + try { + //判断cronExpression是否有效 + expression = "$second $minute $hour $day $month $week $year" + Log.d(TAG, "afterYearChanged expression:$expression") + CronExpression.validateExpression(expression) + } catch (e: Exception) { + XToastUtils.error("Cron表达式无效:" + e.message, 30000) + return + } + + when { + year == "*" -> { + binding!!.rbYearTypeAll.isChecked = true + } + + year == "?" -> { + binding!!.rbYearTypeNotAssigned.isChecked = true + } + + year.contains("/") -> { + val yearArray = year.split("/") + binding!!.etYearIntervalStart.setText(yearArray.getOrNull(0) ?: "2023") + binding!!.etYearInterval.setText(yearArray.getOrNull(1) ?: "2") + binding!!.rbYearTypeInterval.isChecked = true + } + + year.contains(",") -> { + val yearList = restoreMergedItems(year, "%d") + Log.d(TAG, "yearList:$yearList") + binding!!.flowlayoutMultiSelectYear.setSelectedItems(yearList) + binding!!.rbYearTypeAssigned.isChecked = true + selectedYearList = yearList.joinToString(",") + } + + year.contains("-") -> { + val yearArray = year.split("-") + binding!!.etYearCyclicFrom.setText(yearArray.getOrNull(0) ?: "1970") + binding!!.etYearCyclicTo.setText(yearArray.getOrNull(1) ?: "2099") + binding!!.rbYearTypeCyclic.isChecked = true + } + + yearList.indexOf(year) != -1 -> { + binding!!.flowlayoutMultiSelectYear.setSelectedItems(year) + binding!!.rbYearTypeAssigned.isChecked = true + selectedYearList = year + } + + else -> { + binding!!.rbYearTypeAll.isChecked = true + } + } + } + + //获取选中的项目 + private fun getSelectedItems(parent: FlowTagLayout, selectedList: List, dataType: Int = 0): String { + if (selectedList.isEmpty()) return "" + + val selectedNumList = mutableListOf() + for (index in selectedList) { + selectedNumList.add(parent.adapter.getItem(index).toString()) + } + val mergedList = when (dataType) { + 2 -> mergeContinuousEnum(selectedNumList) + 1 -> mergeContinuousItems(selectedNumList, "%02d") + else -> mergeContinuousItems(selectedNumList) + } + return mergedList.joinToString(",") + } + + //合并连续的枚举值 + private fun mergeContinuousEnum(input: List): List { + if (input.isEmpty()) return emptyList() + + val result = mutableListOf() + val enumValues = if (input.firstOrNull() in monthList) monthList else weekList + + var start = enumValues.indexOf(input[0]) + var end = enumValues.indexOf(input[0]) + + for (i in 1 until input.size) { + val currentIndex = enumValues.indexOf(input[i]) + if (currentIndex == end + 1) { + end = currentIndex + } else { + if (start == end) { + result.add(enumValues[start]) + } else { + result.add("${enumValues[start]}-${enumValues[end]}") + } + start = currentIndex + end = currentIndex + } + } + + if (start == end) { + result.add(enumValues[start]) + } else { + result.add("${enumValues[start]}-${enumValues[end]}") + } + + return result + } + + //合并连续的数字 + private fun mergeContinuousItems(input: List, stringFormat: String = "%d"): List { + if (input.isEmpty()) return emptyList() + + val items = input.map { it.toInt() }.sorted() + + val result = mutableListOf() + var start = items[0] + var end = items[0] + + for (i in 1 until items.size) { + if (items[i] == end + 1) { + end = items[i] + } else { + if (start == end) { + result.add(String.format(stringFormat, start)) + } else { + result.add(String.format(stringFormat, start) + "-" + String.format(stringFormat, end)) + } + start = items[i] + end = items[i] + } + } + + if (start == end) { + result.add(String.format(stringFormat, start)) + } else { + result.add(String.format(stringFormat, start) + "-" + String.format(stringFormat, end)) + } + + return result + } + + //还原被合并的连续数字 + private fun restoreMergedItems(mergedString: String, stringFormat: String = "%d"): List { + if (mergedString.isEmpty()) return emptyList() + + val items = mutableListOf() + val ranges = mergedString.split(",") + + for (range in ranges) { + val rangeParts = range.split("-") + if (rangeParts.size == 1) { + items.add(rangeParts[0]) + } else if (rangeParts.size == 2) { + val start = rangeParts[0].toInt() + val end = rangeParts[1].toInt() + for (i in start..end) { + items.add(String.format(stringFormat, i)) + } + } + } + + return items + } + + //检查设置 + @SuppressLint("SetTextI18n") + private fun checkSetting(): CronSetting { + second = binding!!.etSecond.text.toString().trim() + minute = binding!!.etMinute.text.toString().trim() + hour = binding!!.etHour.text.toString().trim() + day = binding!!.etDay.text.toString().trim() + month = binding!!.etMonth.text.toString().trim() + week = binding!!.etWeek.text.toString().trim() + year = binding!!.etYear.text.toString().trim() + + expression = "$second $minute $hour $day $month $week $year" + description = "" + Log.d(TAG, "checkSetting, expression:$expression") + + //判断cronExpression是否有效 + CronExpression.validateExpression(expression) + + //生成cron表达式描述 + val options = Options() + options.isTwentyFourHourTime = true + //TODO:支持多语言 + val locale = Locale.getDefault() + //Chinese, Japanese, Korean and other East Asian languages have no spaces between words + options.isNeedSpaceBetweenWords = locale == Locale("zh") || locale == Locale("ja") || locale == Locale("ko") + description = CronExpressionDescriptor.getDescription(expression, options, locale) + + return CronSetting(expression, description) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/receiver/AlarmReceiver.kt b/app/src/main/java/com/idormy/sms/forwarder/receiver/AlarmReceiver.kt new file mode 100644 index 00000000..a1d6ea23 --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/receiver/AlarmReceiver.kt @@ -0,0 +1,23 @@ +package com.idormy.sms.forwarder.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import com.idormy.sms.forwarder.database.entity.Task + +@Suppress("PropertyName") +class AlarmReceiver : BroadcastReceiver() { + + val TAG: String = AlarmReceiver::class.java.simpleName + + override fun onReceive(context: Context, intent: Intent) { + val task = intent.getParcelableExtra("task") + + // 根据任务信息执行相应操作 + if (task != null) { + Log.d(TAG, "onReceive task $task") + // 处理任务逻辑,例如执行特定操作或者更新界面 + } + } +} 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 4c134ddd..81d7f46e 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 @@ -476,33 +476,74 @@ var CLIENT_FRAGMENT_LIST = listOf( ) //自动任务 -var TASK_FRAGMENT_LIST = listOf( +const val KEY_TEST_CONDITION = "key_test_condition" +const val KEY_EVENT_DATA_CONDITION = "event_data_condition" +const val KEY_BACK_CODE_CONDITION = 1000 +const val KEY_BACK_DATA_CONDITION = "back_data_condition" + +const val KEY_TEST_ACTION = "key_test_action" +const val KEY_EVENT_DATA_ACTION = "event_data_action" +const val KEY_BACK_CODE_ACTION = 2000 +const val KEY_BACK_DATA_ACTION = "back_data_action" + +const val TASK_CRON = 0 +var TASK_CONDITION_FRAGMENT_LIST = listOf( PageInfo( - getString(R.string.dingtalk_robot), - "com.idormy.sms.forwarder.fragment.senders.DingtalkGroupRobotFragment", + getString(R.string.task_cron), + "com.idormy.sms.forwarder.fragment.condition.CronFragment", "{\"\":\"\"}", CoreAnim.slide, - R.drawable.icon_dingtalk + R.drawable.auto_task_icon_cron ), PageInfo( getString(R.string.email), "com.idormy.sms.forwarder.fragment.senders.EmailFragment", "{\"\":\"\"}", CoreAnim.slide, - R.drawable.icon_email + R.drawable.auto_task_icon_battery ), PageInfo( getString(R.string.bark), "com.idormy.sms.forwarder.fragment.senders.BarkFragment", "{\"\":\"\"}", CoreAnim.slide, - R.drawable.icon_bark + R.drawable.auto_task_icon_charge ), PageInfo( getString(R.string.webhook), "com.idormy.sms.forwarder.fragment.senders.WebhookFragment", "{\"\":\"\"}", CoreAnim.slide, - R.drawable.icon_webhook + R.drawable.auto_task_icon_wlan + ), +) +var TASK_ACTION_FRAGMENT_LIST = listOf( + PageInfo( + getString(R.string.task_cron), + "com.idormy.sms.forwarder.fragment.condition.CronFragment", + "{\"\":\"\"}", + CoreAnim.slide, + R.drawable.auto_task_icon_cron + ), + PageInfo( + getString(R.string.email), + "com.idormy.sms.forwarder.fragment.senders.EmailFragment", + "{\"\":\"\"}", + CoreAnim.slide, + R.drawable.auto_task_icon_battery + ), + PageInfo( + getString(R.string.bark), + "com.idormy.sms.forwarder.fragment.senders.BarkFragment", + "{\"\":\"\"}", + CoreAnim.slide, + R.drawable.auto_task_icon_charge + ), + PageInfo( + getString(R.string.webhook), + "com.idormy.sms.forwarder.fragment.senders.WebhookFragment", + "{\"\":\"\"}", + CoreAnim.slide, + R.drawable.auto_task_icon_wlan ), ) \ No newline at end of file diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/HttpServerUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/HttpServerUtils.kt index fb94cd31..b2014f86 100644 --- a/app/src/main/java/com/idormy/sms/forwarder/utils/HttpServerUtils.kt +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/HttpServerUtils.kt @@ -148,6 +148,7 @@ class HttpServerUtils private constructor() { cloneInfo.senderList = Core.sender.getAllNonCache() cloneInfo.ruleList = Core.rule.getAllNonCache() cloneInfo.frpcList = Core.frpc.getAllNonCache() + cloneInfo.taskList = Core.task.getAllNonCache() return cloneInfo } @@ -187,6 +188,13 @@ class HttpServerUtils private constructor() { Core.frpc.insert(frpc) } } + //Task配置 + Core.task.deleteAll() + if (!cloneInfo.taskList.isNullOrEmpty()) { + for (task in cloneInfo.taskList!!) { + Core.task.insert(task) + } + } true } catch (e: Exception) { e.printStackTrace() diff --git a/app/src/main/java/com/idormy/sms/forwarder/utils/task/CronUtils.kt b/app/src/main/java/com/idormy/sms/forwarder/utils/task/CronUtils.kt new file mode 100644 index 00000000..e09842be --- /dev/null +++ b/app/src/main/java/com/idormy/sms/forwarder/utils/task/CronUtils.kt @@ -0,0 +1,66 @@ +package com.idormy.sms.forwarder.utils.task + +import android.annotation.SuppressLint +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import com.idormy.sms.forwarder.database.entity.Task +import com.idormy.sms.forwarder.receiver.AlarmReceiver + +@Suppress("unused") +class CronUtils { + companion object { + + @SuppressLint("StaticFieldLeak") + private lateinit var context: Context + + fun initialize(context: Context) { + this.context = context.applicationContext + } + + fun updateTaskAndScheduleAlarm(task: Task) { + val oldTask = getOldTask(task.id) // 获取旧的任务信息 + cancelAlarm(oldTask) // 取消旧任务的定时器 + + updateTaskInDatabase(task) // 更新任务信息(例如,更新数据库中的任务信息) + scheduleAlarm(task) // 设置新的定时器 + } + + private fun cancelAlarm(task: Task?) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val alarmIntent = Intent(context, AlarmReceiver::class.java) + val requestCode = task?.id?.toInt() ?: -1 + + val pendingIntent = PendingIntent.getBroadcast(context, requestCode, alarmIntent, PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE) + pendingIntent?.let { + alarmManager.cancel(it) + it.cancel() + } + } + + private fun scheduleAlarm(task: Task) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val alarmIntent = Intent(context, AlarmReceiver::class.java) + alarmIntent.putExtra("task", task) + val requestCode = task.id.toInt() + val pendingIntent = PendingIntent.getBroadcast(context, requestCode, alarmIntent, PendingIntent.FLAG_IMMUTABLE) + //val now = Calendar.getInstance() + val nextExecutionTime = task.nextExecTime.time + + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, nextExecutionTime, pendingIntent + ) + } + + private fun getOldTask(taskId: Long): Task { + // 实现获取旧任务信息的逻辑 + // 返回旧任务信息(Task对象) + return Task() + } + + private fun updateTaskInDatabase(task: Task) { + // 实现更新数据库中任务信息的逻辑 + } + } +} diff --git a/app/src/main/res/drawable/auto_task_icon_battery.xml b/app/src/main/res/drawable/auto_task_icon_battery.xml new file mode 100644 index 00000000..8d4cd9a2 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_battery.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_bluetooth.xml b/app/src/main/res/drawable/auto_task_icon_bluetooth.xml new file mode 100644 index 00000000..d4030138 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_bluetooth.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_bluetooth_device.xml b/app/src/main/res/drawable/auto_task_icon_bluetooth_device.xml new file mode 100644 index 00000000..d92e6cd9 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_bluetooth_device.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_charge.xml b/app/src/main/res/drawable/auto_task_icon_charge.xml new file mode 100644 index 00000000..8febafcf --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_charge.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_connect_wlan.xml b/app/src/main/res/drawable/auto_task_icon_connect_wlan.xml new file mode 100644 index 00000000..73cf2054 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_connect_wlan.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_cron.xml b/app/src/main/res/drawable/auto_task_icon_cron.xml new file mode 100644 index 00000000..f41c9f50 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_cron.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_disconnect_bluetooth_device.xml b/app/src/main/res/drawable/auto_task_icon_disconnect_bluetooth_device.xml new file mode 100644 index 00000000..a9094394 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_disconnect_bluetooth_device.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_disconnect_wlan.xml b/app/src/main/res/drawable/auto_task_icon_disconnect_wlan.xml new file mode 100644 index 00000000..cabc540b --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_disconnect_wlan.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_incall.xml b/app/src/main/res/drawable/auto_task_icon_incall.xml new file mode 100644 index 00000000..3f80e898 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_incall.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_leave_address.xml b/app/src/main/res/drawable/auto_task_icon_leave_address.xml new file mode 100644 index 00000000..5d58eecb --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_leave_address.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_location.xml b/app/src/main/res/drawable/auto_task_icon_location.xml new file mode 100644 index 00000000..92cb905a --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_location.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_sim.xml b/app/src/main/res/drawable/auto_task_icon_sim.xml new file mode 100644 index 00000000..55bae44b --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_sim.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_to_address.xml b/app/src/main/res/drawable/auto_task_icon_to_address.xml new file mode 100644 index 00000000..5c6416fa --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_to_address.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/auto_task_icon_wlan.xml b/app/src/main/res/drawable/auto_task_icon_wlan.xml new file mode 100644 index 00000000..7fe07907 --- /dev/null +++ b/app/src/main/res/drawable/auto_task_icon_wlan.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_drag.xml b/app/src/main/res/drawable/ic_drag.xml new file mode 100644 index 00000000..5a5f4fa8 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml index 4781397c..35f23660 100644 --- a/app/src/main/res/drawable/ic_edit.xml +++ b/app/src/main/res/drawable/ic_edit.xml @@ -1,10 +1,9 @@ diff --git a/app/src/main/res/drawable/ic_menu_task.xml b/app/src/main/res/drawable/ic_menu_task.xml new file mode 100644 index 00000000..4617246b --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_task.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sim.xml b/app/src/main/res/drawable/ic_sim.xml index 5602b95a..f90e2bfc 100644 --- a/app/src/main/res/drawable/ic_sim.xml +++ b/app/src/main/res/drawable/ic_sim.xml @@ -1,10 +1,20 @@ + + android:viewportWidth="40.0" + android:viewportHeight="50.0"> + android:fillAlpha="0.8" + android:fillColor="#ffffffff" + android:fillType="evenOdd" + android:pathData="M24.6702,0C26.9147,0 29.0561,0.9429 30.5715,2.5987L37.9014,10.6071C39.2513,12.0821 40,14.009 40,16.0084V44C40,47.3137 37.3137,50 34,50H6C2.6863,50 0,47.3137 0,44V6C0,2.6863 2.6863,0 6,0H24.6702Z + M18,43L14,43v-4h4v4z + M34,43h-4v-4h4v4z + M18,35L14,35v-8h4v8z + M26,43h-4v-8h4v8z + M26,31h-4v-4h4v4z + M34,35h-4v-8h4v8z" + android:strokeAlpha="0.8" /> diff --git a/app/src/main/res/drawable/ic_sim1.xml b/app/src/main/res/drawable/ic_sim1.xml index 5033c3e6..60b62661 100644 --- a/app/src/main/res/drawable/ic_sim1.xml +++ b/app/src/main/res/drawable/ic_sim1.xml @@ -1,10 +1,14 @@ + + android:viewportWidth="40.0" + android:viewportHeight="50.0"> + android:fillAlpha="0.8" + android:fillColor="#ffffffff" + android:fillType="evenOdd" + android:pathData="M24.6702,0C26.9147,0 29.0561,0.9429 30.5715,2.5987L37.9014,10.6071C39.2513,12.0821 40,14.009 40,16.0084V44C40,47.3137 37.3137,50 34,50H6C2.6863,50 0,47.3137 0,44V6C0,2.6863 2.6863,0 6,0H24.6702ZM22.785,13.8333H18.8717L13.33,15.4483V20.3117L17.7633,19.045V38H22.785V13.8333Z" + android:strokeAlpha="0.8" /> diff --git a/app/src/main/res/drawable/ic_sim2.xml b/app/src/main/res/drawable/ic_sim2.xml index 5f95d476..2f5732a7 100644 --- a/app/src/main/res/drawable/ic_sim2.xml +++ b/app/src/main/res/drawable/ic_sim2.xml @@ -1,10 +1,14 @@ + + android:viewportWidth="40.0" + android:viewportHeight="50.0"> + android:fillAlpha="0.8" + android:fillColor="#ffffffff" + android:fillType="evenOdd" + android:pathData="M30.5715,2.5987C29.0561,0.9429 26.9147,0 24.6702,0H6C2.6863,0 0,2.6863 0,6V44C0,47.3137 2.6863,50 6,50H34C37.3137,50 40,47.3137 40,44V16.0084C40,14.009 39.2513,12.0821 37.9014,10.6071L30.5715,2.5987ZM20.1471,13C17.7983,13 15.7604,13.7514 14.3071,15.1044C12.8518,16.4593 12,18.4019 12,20.7467V20.9918H16.7129V20.7467C16.7129,19.6857 17.0933,18.8354 17.7036,18.2496C18.3155,17.6623 19.1728,17.3274 20.1471,17.3274C22.0709,17.3274 23.3606,18.4956 23.3606,20.0899C23.3606,21.317 22.9263,22.2977 21.5103,23.9022L21.5087,23.9041L12.3677,34.4825V38H27.7794V33.7763H18.6271L24.6667,26.89C26.6563,24.638 28,22.6766 28,20.0899C28,16.0219 24.5918,13 20.1471,13Z" + android:strokeAlpha="0.8" /> diff --git a/app/src/main/res/layout/adapter_task_setting_item.xml b/app/src/main/res/layout/adapter_task_setting_item.xml new file mode 100644 index 00000000..736e1de7 --- /dev/null +++ b/app/src/main/res/layout/adapter_task_setting_item.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_tasks_card_view_list_item.xml b/app/src/main/res/layout/adapter_tasks_card_view_list_item.xml new file mode 100644 index 00000000..889b4801 --- /dev/null +++ b/app/src/main/res/layout/adapter_tasks_card_view_list_item.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_task_action_bottom_sheet.xml b/app/src/main/res/layout/dialog_task_action_bottom_sheet.xml new file mode 100644 index 00000000..a0101dba --- /dev/null +++ b/app/src/main/res/layout/dialog_task_action_bottom_sheet.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_task_condition_bottom_sheet.xml b/app/src/main/res/layout/dialog_task_condition_bottom_sheet.xml new file mode 100644 index 00000000..ca11da57 --- /dev/null +++ b/app/src/main/res/layout/dialog_task_condition_bottom_sheet.xml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tasks.xml b/app/src/main/res/layout/fragment_tasks.xml new file mode 100644 index 00000000..73a33ea6 --- /dev/null +++ b/app/src/main/res/layout/fragment_tasks.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tasks_cron.xml b/app/src/main/res/layout/fragment_tasks_cron.xml new file mode 100644 index 00000000..1fd9da16 --- /dev/null +++ b/app/src/main/res/layout/fragment_tasks_cron.xml @@ -0,0 +1,1259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tasks_edit.xml b/app/src/main/res/layout/fragment_tasks_edit.xml new file mode 100644 index 00000000..4dee51bc --- /dev/null +++ b/app/src/main/res/layout/fragment_tasks_edit.xml @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_add_action.xml b/app/src/main/res/layout/item_add_action.xml new file mode 100644 index 00000000..b5c4e1ea --- /dev/null +++ b/app/src/main/res/layout/item_add_action.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_add_condition.xml b/app/src/main/res/layout/item_add_condition.xml new file mode 100644 index 00000000..c08f5f60 --- /dev/null +++ b/app/src/main/res/layout/item_add_condition.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_drawer.xml b/app/src/main/res/menu/menu_drawer.xml index ebf29a78..9437d1ec 100644 --- a/app/src/main/res/menu/menu_drawer.xml +++ b/app/src/main/res/menu/menu_drawer.xml @@ -31,6 +31,12 @@ android:id="@+id/other" android:checkableBehavior="single"> + + System App + + + My Task + Task Center + + \@qq.com \@foxmail.com diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index dabb548b..9ba09ee2 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -10,7 +10,7 @@ Rules Settings - Task + Task Server Client Frpc @@ -1030,7 +1030,6 @@ User ID Auto delete logs N days ago 0=disabled, scan when battery change - Day Safety Measures The client and server must be the same. It is strongly recommended to enable encryption when accessing the public network. None @@ -1079,5 +1078,85 @@ Insert location info into forwarded msg. UID - Please select task type + Name/Status + Task Name + Description + If + Influenced by the first condition, the others serve as determinants. + then execute. + Allow multiple execution actions, with each execution result being independent of the others. + Last Exec Time + Next Exec Time + Add Task + Edit Task + Clone Task + Delete confirmation + Are you sure to delete this task? + The task has deleted. + 添加条件 + 例如:如果电量低于20%时 + 继续添加条件 + 添加动作 + 例如:禁用所有转发通道 + 继续添加动作 + + Please select condition + Please select action + Cron + + Second + Minute + Hour + Day + Month + Week + Year + Cron Expression + Quartz Cron Expression + Every Second + Every Minute + Every Hour + Every Day + Every Month + Every Week + Every Year + Cyclic + From + From week + To + Start + End + Starting from + second, execute every + seconds. + Starting from + minute, execute every + minutes. + Starting from + hour, execute every + hours. + Starting from + day, execute every + days. + Starting from + month, execute every + months. + Starting from + year, execute every + years. + Assigned + Not Assigned + Recent Days + The nearest working day to the + day of each month. + Last day of month + Last day of month recent days + + 周的星期 + The last [day of the week] of this month. + The last week + of this month. + Cron Expression Test Result + Cron expression is invalid:\n%s + The next %s execution times:\n%s diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 0d598342..cf50b0ed 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -80,6 +80,12 @@ 系统应用 + + + 我的任务 + 任务中心 + + \@qq.com \@foxmail.com diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d10f9b7..8f0b2009 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,7 +10,7 @@ 转发规则 通用设置 - 自动任务·快捷指令 + 自动任务·快捷指令 主动控制·服务端 主动控制·客户端 内网穿透·Frpc @@ -1031,7 +1031,6 @@ User ID 自动删除N天前的转发记录 0=禁用,触发机制:每次电量变化时扫描 - 安全措施 客户端与服务端必须一致,强烈建议公网访问时启用加密 不需要 @@ -1080,5 +1079,85 @@ 在转发信息中插入手机的当前定位信息 UID - 请选择自动任务类型 + 任务名称/状态 + 任务名称 + 任务描述 + 如果 + 由第一个条件触发,其他条件作为判断 + 就执行 + 允许添加多个执行动作,执行结果互不干扰 + 上次执行时间 + 下次执行时间 + 新建任务 + 编辑任务 + 克隆任务 + 删除任务操作确认 + 删除任务操作确认 + 删除任务操作确认 + 添加条件 + 例如:如果电量低于20%时 + 继续添加条件 + 添加动作 + 例如:禁用所有转发通道 + 继续添加动作 + + 请选择条件 + 请选择动作 + 定时任务 + + + + + + + + + Cron表达式 + 采用 Quartz Cron 表达式 + 每秒钟 + 每分钟 + 每小时 + 每日 + 每月 + 每周 + 每年 + 周期 + + 从星期 + + 起始 + 结束 + + 秒开始,每 + 秒钟执行一次 + + 分开始,每隔 + 分钟执行一次 + + 时开始,每隔 + 小时执行一次 + + 日开始,每隔 + 天执行一次 + + 月开始,每隔 + 月执行一次 + + 年开始,每隔 + 年执行一次 + 指定 + 不指定 + 最近工作日 + 每月 + 号最近的那个工作日 + 本月最后一天 + 本月最后一个工作日 + + 周的星期 + 本月最后 + 本月最后一个星期 + + Cron表达式测试结果 + Cron表达式无效:\n%s + 最近 %s 次运行时间:\n%s diff --git a/app/src/main/res/values/styles_widget.xml b/app/src/main/res/values/styles_widget.xml index d28e8cad..4c42df5d 100644 --- a/app/src/main/res/values/styles_widget.xml +++ b/app/src/main/res/values/styles_widget.xml @@ -96,6 +96,29 @@ + + + +