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 @@
+
+
+
+