Repository: AndroidCoderPeng/AutoDingding
Branch: master
Commit: 72197fe8c815
Files: 100
Total size: 283.9 KB
Directory structure:
gitextract_22m6e9zb/
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── DailyTask.jks
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── assets/
│ │ └── QuestionAndAnswer.json
│ ├── cpp/
│ │ ├── CMakeLists.txt
│ │ └── daily_task.cpp
│ ├── java/
│ │ └── com/
│ │ └── pengxh/
│ │ └── daily/
│ │ └── app/
│ │ ├── DailyTaskApplication.kt
│ │ ├── adapter/
│ │ │ └── DailyTaskAdapter.kt
│ │ ├── extensions/
│ │ │ ├── Context.kt
│ │ │ ├── DailyTaskBean.kt
│ │ │ ├── Int.kt
│ │ │ ├── List.kt
│ │ │ ├── String.kt
│ │ │ └── View.kt
│ │ ├── model/
│ │ │ ├── ExportDataModel.java
│ │ │ └── QuestionAnAnswerModel.java
│ │ ├── retrofit/
│ │ │ ├── RetrofitService.kt
│ │ │ └── RetrofitServiceManager.kt
│ │ ├── service/
│ │ │ ├── CaptureImageService.kt
│ │ │ ├── CountDownTimerService.kt
│ │ │ ├── FloatingWindowService.kt
│ │ │ ├── ForegroundRunningService.kt
│ │ │ └── NotificationMonitorService.kt
│ │ ├── sqlite/
│ │ │ ├── DailyTaskDataBase.java
│ │ │ ├── DatabaseWrapper.kt
│ │ │ ├── bean/
│ │ │ │ ├── DailyTaskBean.java
│ │ │ │ ├── EmailConfigBean.java
│ │ │ │ └── NotificationBean.java
│ │ │ └── dao/
│ │ │ ├── DailyTaskBeanDao.java
│ │ │ ├── EmailConfigBeanDao.java
│ │ │ └── NotificationBeanDao.java
│ │ ├── ui/
│ │ │ ├── MainActivity.kt
│ │ │ ├── MessageChannelActivity.kt
│ │ │ ├── QuestionAndAnswerActivity.kt
│ │ │ ├── SettingsActivity.kt
│ │ │ └── TaskConfigActivity.kt
│ │ ├── utils/
│ │ │ ├── AlarmScheduler.kt
│ │ │ ├── ApplicationEvent.kt
│ │ │ ├── Constant.kt
│ │ │ ├── DailyTask.kt
│ │ │ ├── EmailAuthenticator.kt
│ │ │ ├── EmailManager.kt
│ │ │ ├── GestureController.kt
│ │ │ ├── HttpRequestManager.kt
│ │ │ ├── LogFileManager.kt
│ │ │ ├── MaskViewController.kt
│ │ │ ├── MessageDispatcher.kt
│ │ │ ├── ProjectionSession.kt
│ │ │ ├── TaskDataManager.kt
│ │ │ ├── TaskResetReceiver.kt
│ │ │ ├── TaskScheduler.kt
│ │ │ ├── TimeKit.kt
│ │ │ ├── TimeoutTimerManager.kt
│ │ │ └── WatermarkDrawable.kt
│ │ └── vm/
│ │ └── MessageViewModel.kt
│ └── res/
│ ├── drawable/
│ │ ├── bg_solid_layout_white_16.xml
│ │ ├── divider_gradient.xml
│ │ ├── ic_arrow_right.xml
│ │ ├── ic_clear.xml
│ │ ├── ic_ding_ding.xml
│ │ ├── ic_fei_shu.xml
│ │ ├── ic_menu_add.xml
│ │ ├── ic_menu_settings.xml
│ │ ├── ic_sand.xml
│ │ ├── ic_title_right_black.xml
│ │ └── ic_wei_xin.xml
│ ├── layout/
│ │ ├── activity_main.xml
│ │ ├── activity_message_channel.xml
│ │ ├── activity_question_and_answer.xml
│ │ ├── activity_settings.xml
│ │ ├── activity_task_config.xml
│ │ ├── bottom_sheet_layout_select_time.xml
│ │ ├── item_app_rv_l.xml
│ │ ├── item_daily_task_rv_l.xml
│ │ ├── item_notice_rv_l.xml
│ │ ├── item_q_a_rv_l.xml
│ │ ├── item_task_rv_g.xml
│ │ └── window_floating.xml
│ ├── menu/
│ │ ├── email_config_top_bar_menu.xml
│ │ └── main_top_bar_menu.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ └── values/
│ ├── colors.xml
│ ├── dimens.xml
│ ├── ic_launcher_background.xml
│ ├── strings.xml
│ ├── styles.xml
│ └── themes.xml
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Built application files
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
.kotlin/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
/apk
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# DailyTask(一个还算可以的无人值守方案)
## 1. 本软件完全免费!
## 2. 近期发现有人在咸鱼私自倒卖本软件,请勿购买!如有购买,请联系卖家退款!
## 3.另外,由于有人倒卖本工具,所以,Github不再提供安装包,如有能力可自行编译源码,否则进QQ群获取。
# 使用须知
## 1. 本软件本地运行,无服务器备份,不存在窃取隐私和泄密!
## 2. 因为有远程指令功能,所以必须监听QQ、微信等及时通讯软件消息,才能正常执行远程指令,并且监听的是通讯软件的小号。如果介意此功能,请绕道,不必进群获取安装包了!
## 3. 另外,在质疑别人之前,请自行补充下相关知识,别在外面丢人现眼!请务必先阅读本软件的使用说明,并确认是否了解本软件的功能和用法!



# 真·气的肝疼!如果哪天这个项目没了,应该就是类似的人多了,那么各位就自求多福吧!
# 软件介绍
1. Kotlin+Java混编实现的打卡小工具,解决您上班途中迟到问题,只需一部备用手机置于公司工位,设置一下上下班打卡时间即可。
2. 兼容到 `Android 8` ~ `Android 16` 或者 `鸿蒙 4.0` 系统,小米的澎湃系统需要自行测试。
3. 本软件最开始的本意是方便自己,后来由于换了新单位,此款软件不再使用,故选择开源,如有不到之处还请谅解。
4. 本软件属于无人值守方案,不会修改各类签到软件内部逻辑,也不会修改手机位置,请注意!!!
5. 本软件仅限学习和内部使用,严禁商用和用作其他非法用途,如有违反,与本人无关!!!
6. 对目前功能不满意或者想加功能的,有能力的可以自行下载源码修改,也可以在群里反馈了等我发布新版本。
7. 手机不能灭屏。灭屏状态下,常驻通知服务会被系统回收,导致无法打卡。另外锁屏后再解锁,在部分手机上并不会直接进入桌面,会无法调起软件。
8. 主界面按音量`减小键`,会开启伪装灭屏模式,该模式下不影响打卡,再按一次,会退出伪装模式。另外伪灭屏模式支持手势触发(上下滑动屏幕即可)。
9. 默认每天都会打卡,如果不需要可以发送`暂停循环`指令,远程控制任务执行(大号给小号发,QQ、微信、支付宝、TIM都支持),具体操作流程图见下文。
10. 如果要用,最好先自行测试几天,稳定确认没问题之后再使用,并请做好隐蔽工作,不要被人发现!如果被发现,后果自负。
------
# 最新版本 2.3.0.0 —— 更新时间:2026年4月2日09点28分
1. 微调主界面样式,任务列表添加展开和收缩动画效果
2. 修复打卡成功后定时器未取消的问题
3. 修复删除任务后数据不一致问题
4. 重构每日任务自动重置功能
5. 全面支持`钉钉`、`飞书`、`企业微信`、`移动办公M3`等签到软件。
6. 添加打卡后自动截屏功能,并支持远程截屏(见上文指令`10`)
7. 添加图片消息和附件邮件发送功能
8. 添加状态查询功能(见上文指令`9`)
9. 添加任务配置分享功能,支持QQ、微信、TIM、支付宝和剪切板,方便任务配置一键导出
------
# 使用步骤(**目标应用必须要支持极速打卡且已设置为极速打卡**):
1. [x] 打开`DailyTask`,会自动检测悬浮窗权限,找到`DailyTask`软件,打开悬浮窗权限即可。
2. [x] 在手机`设置`里面打开通知中心,然后找到`DailyTask`,点进去后打开允许通知开关。
3. [x] 在`DailyTask`的`设置`里面设置`结果来源`,默认为`通知监听`(只适用于`钉钉`),`截屏服务`
理论上可以获取任何应用的打卡结果。
4. [x] 消息渠道设置
1. 企业微信:登录企业微信,随便拉个朋友或者自己弄个小号建立一个群聊,然后点击群聊右上角,进去之后在那个界面找到
`消息推送`,点击进去之后在那个界面配置一下名称,点击下面的`webhook地址`复制出来后点击保存,最后把复制出来的
`webhook地址`最后面的key值填入到`DailyTask`的消息渠道的企业微信渠道里面即可。
2. QQ邮箱:输入发件箱以及发件箱的授权码(不是邮箱密码!!!),然后填写收件箱,其他的随意。另外,发件箱和收件箱可以是同一个邮箱。
5. [x] 在`DailyTask`的`设置`打开通知监听开关(如果未打开此开关,此开关底部会有一行红色小字)。找到
`DailyTask`软件,打开即可。
6. [x] 如果钉钉无法监听到打卡结果,或者飞书、企业微信这种没有打卡通知的,一定要把这个打开!弹窗显示选择
`整个屏幕`即可。
7. [x] 在你设置好消息渠道并且已经打开截屏服务,那么可以通过`截屏测试`
来确认截屏服务以及消息渠道是否正常工作,能收到消息渠道的反馈即为正常。
8. [x] 如果想通过QQ,TIM、微信、支付宝消息唤起目标应用打卡,在`DailyTask`的`设置`
界面点击唤起测试,确认以上应用是否有权限打开目标应用,如果不需要可以跳过此步骤。
好了,基本设置就是这样了。

# 支持的远程指令:
| 序号 | 指令 | 功能 | 是否有邮件通知 |
|:---|:-------|:------------------------------------|---------|
| 1 | `启动` | 启动循环任务(默认每天都会执行) | 是 |
| 2 | `停止` | 停止循环任务(只会停止当天的任务) | 是 |
| 3 | `开始循环` | 循环任务标志位 | 是 |
| 4 | `暂停循环` | 循环任务标志位(收到此指令后,会永远暂停执行,除非再次收到指令`3`) | 是 |
| 5 | `息屏` | 开启伪灭屏模式 | 否 |
| 6 | `亮屏` | 退出伪灭屏模式 | 否 |
| 7 | `考勤记录` | 导出当天的考勤记录 | 是 |
| 8 | `打卡` | 默认为“打卡”,如果自己修改过指令,按修改后的指令发送 | 否 |
| 9 | `状态查询` | 获取当前APP状态,包括任务、服务、监听状态、电量、版本、日期等 | 是 |
| 10 | `截屏` | 截取一张目标应用的屏幕,并通过消息渠道反馈给用户 | 是 |
注意:
- 如果要每天打卡,那就不必关注指令`3`和`4`。
```mermaid
graph LR
指令1 --> 任务开始 --> 执行任务 --> 当天任务结束 --> 自动重置任务 --> 任务开始
```
- 如果要控制任务执行的日期,请结合指令`1`、`3`和`4`。
```mermaid
graph LR
指令1 --> 判断A{循环执行任务}
判断A --> 是 --> 同上面的流程
判断A --> 否 --> 任务开始 --> 执行任务 --> 任务结束
指令3和4 --> 判断A
```
## 如果还有问题,请加QQ群,群内只回答没在此文档提到的问题,其余问题自行看文档,一定要仔细看完!!!:
- 560354109(①群)——已满
- 643595483(②群)——已满
- 377923252(③群)
> ③群的安装包要比①群和②群的迟一个版本
# 已知会被检测到作弊的原因:
| 序号 | 原因 |
|:---|:-------------------------------|
| 1 | 手机已经root(被检测到作弊的概率极大) |
| 2 | 使用了模拟定位软件试图修改打卡位置(被检测到作弊的概率极大) |
| 3 | 使用了向日葵等远程远程控制软件打开(被检测到作弊的概率极大) |
| 4 | 试图使用adb命定模拟手指点击打卡(被检测到作弊的概率极大) |
| 5 | 手机开启了无障碍服务 |
| 6 | 手机数据线连着电脑 |
# 历史版本看这里:
| 版本号 | 版本说明 |
|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 2.0.0 | 1. 全新版本,全新的界面,全新功能!支持每日循环打卡,每日每次打卡时间会自动在设定的时间点5分钟内随机选择一个时间点打卡
2. 解决1.+版本遗留的问题 |
| 2.0.1 | 1. 解决QQ邮箱、163邮箱、126邮箱、yeah邮箱发送邮件失败问题 |
| 2.0.2 | 1. 优化通知监听服务和通知缓存逻辑 |
| 2.0.3 | 1. 修复倒计时任务进度条重叠问题
2. 优化小概率崩溃问题 |
| 2.0.4 | 1. 添加远程启动和停止每日任务功能(`此功能必须开启通知监听,否则指令无效`)。开始每日任务指令:`启动`。停止每日任务指令:`停止`。
2. 修复部分手机打完卡状态栏常亮问题 |
| 2.0.5.1 | 1. 升级AGP,提升targetSdk到36(Android 15),适配Android 15版本新特性。
2. 更改数据持久化框架,使用官方Room框架 |
| 2.0.6 | 1. 重构应用主题样式。
2. 增加自定义超时时间功能。
3. 优化循环任务启动和停止的逻辑与提示信息 |
| 2.1.0 | 1. 优化邮件发送失败的错误处理和消息显示
2. 优化程序前台保活服务
3. 调整每日任务界面,去掉顶部实时计时显示
4. 新增随机时间开关,用户可自行控制是否需要生成随机任务时间点
5. 新增任务计时后台服务,解决任务计时延迟问题
6. 新增任务执行邮件通知
7. 新增伪灭屏状态下拦截电源键并添加时钟显示,让手机看起来更像是真的进入休眠 |
| 2.1.1.0 | 1. 修改前台服务通知标题
2. 优化从目标应用返回软件主页面的逻辑
3. 优化保活服务和后台计时服务
4. 优化任务状态更新逻辑 |
| 2.2.0.0 | 1. 添加每日任务重置时间点设置,默认每天0点重置
2. 添加下拉刷新任务列表功能,解决删除任务小概率会失败的问题
3. 重构消息处理机制
4. 优化邮箱配置检查机制
5. 调整应用界面UI效果 |
| 2.2.1.0 | 1. 主界面显示蒙层时,时钟颜色改为70%透明度白色,并添加随机变换时钟位置动画,降低烧屏风险
2. 修改通知邮件的任务时间为实际时间
3. 添加随机时间范围自定义功能,默认为5分钟 |
| 2.2.2.1 | 1. 删除悬浮窗开关,改为强制开启(不开启会导致无法进行循环任务)
2. 优化邮箱配置判断逻辑,改为不设置邮箱也能正常执行任务
3. 简化邮箱配置,去掉其他邮箱支持,发件箱只支持QQ邮箱 |
| 2.2.5.1 | 1. 重构应用主界面
2. 解决应用广播在Android 13以上版本无法收到的问题
3. 解决邮箱配置Session缓存导致邮件发送失败的问题
4. 解决因部分指令相同前缀导致指令错误执行的问题
5. 解决内部通信消息混乱的问题
6.优化每日任务执行和通知监听服务以及悬浮窗启动逻辑
7.优化伪灭屏显示效果
8.增加5条指令——【指令:`考勤记录`】、【指令:`息屏`】、【指令:`亮屏`】、【指令:`开始循环`】、【指令:`暂停循环`】
9.增加手势开启伪灭屏【单手指从上到下滑动——开启,单手指从下到上滑动——关闭】,并支持选择是否开启,默认关闭 |
| 2.2.6.5 | 1. 解决执行完任务后原有任务列表会停止的问题
2. 支持导出/导入所有信息(任务+配置)
3. 部分界面添加免费声明水印
4. 添加企业微信消息通知渠道,可选择将原来的邮件通知转成企业微信推送
5. 添加可选择目标应用入口,弱支持企业微信和飞书 |
# 打卡结果如下:
| 打卡结果 | 说明 |
|:-----|:----------------------------------------------------------------------------|
| 成功 |  |
| 失败 | 1.账号被自己另一个手机挤下去
2.未设置极速打卡
3.应用内部打卡通知或者手机通知被关闭
4.打卡手机有2个以上 |
================================================
FILE: app/.gitignore
================================================
/build
/daily
================================================
FILE: app/build.gradle
================================================
import java.text.SimpleDateFormat
plugins {
id('com.android.application')
id('org.jetbrains.kotlin.android')
}
android {
namespace 'com.pengxh.daily.app'
compileSdk 36
defaultConfig {
applicationId 'com.pengxh.daily.app'
minSdk 26
targetSdk 36
versionCode 2300
versionName '2.3.0.0'
ndkVersion '21.4.7075529'
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
externalNativeBuild {
cmake {
cppFlags "-std=c++14"
}
}
}
flavorDimensions += 'versionCode'
signingConfigs {
release {
storeFile file('DailyTask.jks')
storePassword '123456789'
keyAlias 'key0'
keyPassword '123456789'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
// GString id = "com.alibaba.android.${randomCode()}"
// productFlavors {
// daily {
// applicationId = id
// }
// }
packagingOptions {
exclude 'META-INF/NOTICE.md'
exclude 'META-INF/LICENSE.md'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
buildConfig true
viewBinding true
}
applicationVariants.configureEach {
outputs.configureEach {
outputFileName = "DT_${getBuildDate()}_${android.defaultConfig.versionName}.apk"
}
}
}
static def randomCode() {
String alphabetsInLowerCase = 'abcdefghijklmnopqrstuvwxyz'
String randomString = ""
for (i in 0..5) {
int randomIndex = Math.random() * alphabetsInLowerCase.length()
randomString += alphabetsInLowerCase[randomIndex]
}
return randomString
}
static def getBuildDate() {
SimpleDateFormat dateFormat = new SimpleDateFormat('yyyyMMdd', Locale.CHINA)
return dateFormat.format(System.currentTimeMillis())
}
dependencies {
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'com.github.AndroidCoderPeng:Kotlin-lite-lib:1.1.7'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
//数据库框架
implementation 'androidx.room:room-runtime:2.8.2'
annotationProcessor 'androidx.room:room-compiler:2.8.2'
//邮件
implementation 'com.sun.mail:android-mail:1.6.8'
implementation 'com.sun.mail:android-activation:1.6.8'
//日期、时间选择器
implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:4.1.14'
implementation 'com.google.code.gson:gson:2.13.2'
//事件总线
implementation 'org.greenrobot:eventbus:3.3.1'
//异常日志记录
implementation 'com.tencent.bugly:crashreport:4.1.9.3'
//返回值转换器
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
//okhttp3日志拦截器
implementation 'com.squareup.okhttp3:logging-interceptor:5.1.0'
//网络请求和接口封装
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.squareup.okhttp3:okhttp:5.1.0'
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/assets/QuestionAndAnswer.json
================================================
[
{
"question": "怎么使用本软件?",
"answer": "本软件不对目标应用做侵入式攻击,不修改定位数据,可以放心使用。只需要设置好任务时间,本软件会在设置的时间点附近调起目标应用实现自动打卡(初次使用会有个弹框提示,“DailyTask”想要打开的目标应用,勾选“始终允许”),但是有个注意点,需要长亮屏幕,因为锁屏后再解锁,并不会直接进入桌面,可能会无法调起软件。"
},
{
"question": "非工作日怎么停止循环任务?",
"answer": "本软件支持远程发送消息控制任务【启动/停止】,前提是你需要在你打卡手机上安装【QQ、微信、TIM、支付宝】其中的任何一个,并用小号登录,开启通知监听,然后用你的大号给这其中的任意一款软件发送【启动】或者【停止】即可,成功与否都会收到一封反馈邮件,注意查看邮箱!"
},
{
"question": "怎么设置打卡?",
"answer": "将目标应用设置为“极速打卡”,打卡时间不要早于你设置目标应用打卡的最早时间,否则会打不上卡。打卡成功与否都会发送一封打卡结果邮件到你自己设置好的邮箱。"
},
{
"question": "为什么本软件不能灭屏使用?",
"answer": "如果手机灭屏,在手机没有被root的情况下,无法通过常规的代码逻辑实现【亮屏+解锁+进入桌面】这三个步骤。首先,亮屏这一步可以通过代码逻辑实现,但是第二步和第三步,由于国内各手机厂商对系统底层的修改,导致有的手机系统能够在不设置锁屏密码的情况下直接进入桌面,但是有的却又不能,那些不能直接进入桌面的系统,通过正规的代码也不能直接进入桌面,所以,为了统一性,本软件不能在灭屏情况下使用。"
},
{
"question": "支持什么邮箱?",
"answer": "发件箱只测试过QQ邮箱,收件箱只要是国内的应该都可以。"
},
{
"question": "手机耗电快怎么处理?",
"answer": "首先将手机亮度可以调最低,其次下班后给手机充着电。或者在主界面按音量减小显示伪装灭屏,再或者用智能插座给手机供电。"
},
{
"question": "怎么删除设置好的定时任务?",
"answer": "长按任务列表项,然后选择删除即可。"
},
{
"question": "收不到打卡成功的邮件,怎么处理?",
"answer": "请先确认好通知栏监听已开启,如不开启将无法监听打卡成功的通知。或者把软件杀掉,然后重启工具并关闭工具的通知监听,最后再重新打开工具的通知监听。随便发条消息到手机,如果能在工具的”所有通知“找到刚刚发的消息,则表示可以正常监听通知,否则重复刚刚的操作"
},
{
"question": "通知栏监听已开启,依旧收不到打卡成功的邮件,怎么处理?",
"answer": "目标应用普通通知能收到,但是收不到打卡通知的,那可能是贵司管理员把打卡通知开关给关了。遇到这种情况的,要么老老实实手动打卡,要么依旧用此工具,只是收不到邮件罢了,问题也不是很大。
详情点这里"
},
{
"question": "打卡失败是什么原因?",
"answer": "1.账号被自己另一个手机挤下去
2.目标应用未设置极速打卡
3.目标应用内部打卡通知或者手机通知被关闭
4.打卡手机有2个以上"
}
]
================================================
FILE: app/src/main/cpp/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.16)
project(daily_task)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加本地 C++ 源码文件
add_library(${PROJECT_NAME} SHARED daily_task.cpp)
# 包含头文件路径
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# 链接库
target_link_libraries(${PROJECT_NAME} android log)
================================================
FILE: app/src/main/cpp/daily_task.cpp
================================================
//
// Created by pengx on 2026/3/4.
//
#include
#include
static const uint8_t TEXT[] = {
0xE5, 0x85, 0x8D, 0xE8, 0xB4, 0xB9,
0xE8,0xBD, 0xAF, 0xE4, 0xBB, 0xB6,
0xEF,0xBC, 0x8C, 0xE8, 0xAF, 0xB7,
0xE5,0x8B, 0xBF, 0xE7, 0x9B, 0xB8,
0xE4,0xBF, 0xA1, 0xE4, 0xBB, 0xBB,
0xE4,0xBD, 0x95, 0xE4, 0xBA, 0xBA,
0xE7,0x9A, 0x84, 0xE4, 0xBB, 0x98,
0xE8,0xB4, 0xB9, 0xE8, 0xA6, 0x81,
0xE6,0xB1, 0x82, 0xEF, 0xBC, 0x81
};
extern "C"
JNIEXPORT jstring JNICALL
Java_com_pengxh_daily_app_utils_DailyTask_getWatermarkText(JNIEnv *env, jobject thiz) {
std::string text(reinterpret_cast(TEXT), sizeof(TEXT));
return env->NewStringUTF(text.c_str());
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/DailyTaskApplication.kt
================================================
package com.pengxh.daily.app
import android.app.Application
import androidx.room.Room.databaseBuilder
import com.pengxh.daily.app.sqlite.DailyTaskDataBase
import com.pengxh.daily.app.utils.LogFileManager
import com.pengxh.kt.lite.utils.SaveKeyValues
import com.tencent.bugly.crashreport.CrashReport
/**
* @author: Pengxh
* @email: 290677893@qq.com
* @date: 2019/12/25 13:19
*/
class DailyTaskApplication : Application() {
companion object {
private lateinit var application: DailyTaskApplication
fun get(): DailyTaskApplication = application
internal fun initApplication(app: DailyTaskApplication) {
application = app
}
}
lateinit var dataBase: DailyTaskDataBase
override fun onCreate() {
super.onCreate()
initApplication(this)
SaveKeyValues.initSharedPreferences(this)
LogFileManager.initLogFile(this)
CrashReport.initCrashReport(this, "ecbdc9baf5", BuildConfig.DEBUG)
dataBase = databaseBuilder(this, DailyTaskDataBase::class.java, "DailyTask.db")
.allowMainThreadQueries()
.build()
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/adapter/DailyTaskAdapter.kt
================================================
package com.pengxh.daily.app.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.pengxh.daily.app.R
import com.pengxh.daily.app.extensions.collapse
import com.pengxh.daily.app.extensions.expand
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean
import com.pengxh.kt.lite.adapter.ViewHolder
import com.pengxh.kt.lite.extensions.convertColor
@SuppressLint("NotifyDataSetChanged")
class DailyTaskAdapter(
private val context: Context,
private val dataBeans: MutableList
) : RecyclerView.Adapter() {
private var layoutInflater = LayoutInflater.from(context)
private var mPosition = -1
private var actualTime = "--:--:--"
private var onItemClickListener: OnItemClickListener? = null
fun updateCurrentTaskState(position: Int) {
this.mPosition = position
notifyDataSetChanged()
}
fun updateCurrentTaskState(position: Int, actualTime: String) {
this.mPosition = position
this.actualTime = actualTime
if (position < 0 || position >= dataBeans.size) {
return
}
notifyDataSetChanged()
}
override fun getItemCount(): Int = dataBeans.size
override fun getItemId(position: Int): Long = position.toLong()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
layoutInflater.inflate(R.layout.item_daily_task_rv_l, parent, false)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val taskBean = dataBeans[position]
holder.setText(R.id.taskTimeView, taskBean.time)
val arrowView = holder.getView(R.id.arrowView)
val actualTimeCardView = holder.getView(R.id.actualTimeCardView)
if (position == mPosition) {
holder.itemView.isSelected = true
holder.setText(R.id.actualTimeView, actualTime)
.setTextColor(R.id.actualTimeView, R.color.theme_color.convertColor(context))
.setTextColor(R.id.taskTimeView, R.color.text_hint_color.convertColor(context))
arrowView.animate().rotation(90f).setDuration(350).start()
if (!actualTimeCardView.isVisible) {
actualTimeCardView.expand()
}
} else {
holder.itemView.isSelected = false
holder.setText(R.id.actualTimeView, "--:--:--")
.setTextColor(R.id.taskTimeView, Color.BLACK)
arrowView.animate().rotation(0f).setDuration(350).start()
if (actualTimeCardView.isVisible) {
actualTimeCardView.collapse()
}
}
holder.itemView.setOnClickListener {
onItemClickListener?.onItemClick(position)
}
holder.itemView.setOnLongClickListener {
onItemClickListener?.onItemLongClick(position)
return@setOnLongClickListener true
}
}
fun refresh(newRows: MutableList) {
dataBeans.clear()
dataBeans.addAll(newRows)
notifyDataSetChanged()
}
interface OnItemClickListener {
fun onItemClick(position: Int)
fun onItemLongClick(position: Int)
}
fun setOnItemClickListener(listener: OnItemClickListener) {
this.onItemClickListener = listener
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/extensions/Context.kt
================================================
package com.pengxh.daily.app.extensions
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationManagerCompat
import com.pengxh.daily.app.utils.ApplicationEvent
import com.pengxh.daily.app.utils.Constant
import com.pengxh.kt.lite.extensions.show
import org.greenrobot.eventbus.EventBus
/**
* 检测通知监听服务是否被授权
* */
fun Context.notificationEnable(): Boolean {
val packages = NotificationManagerCompat.getEnabledListenerPackages(this)
return packages.contains(packageName)
}
/**
* 判断指定包名的应用是否存在
*/
fun Context.isApplicationExist(packageName: String): Boolean {
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
} else {
packageManager.getPackageInfo(packageName, 0)
}
true
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
false
}
}
/**
* 打开指定包名的apk
* @param needCountDown 是否需要倒计时
*/
fun Context.openApplication(needCountDown: Boolean) {
val targetApp = Constant.getTargetApp()
Log.d("Ex-Context", "openApplication: $targetApp")
if (!isApplicationExist(targetApp)) {
"未安装指定的目标软件,无法执行任务".show(this)
EventBus.getDefault().post(ApplicationEvent.StopDailyTask)
return
}
// 跳转目标应用
val intent = Intent(Intent.ACTION_MAIN, null).apply {
addCategory(Intent.CATEGORY_LAUNCHER)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage(targetApp)
}
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0))
} else {
packageManager.queryIntentActivities(intent, 0)
}
if (activities.isNotEmpty()) {
val info = activities.first()
intent.component = ComponentName(info.activityInfo.packageName, info.activityInfo.name)
startActivity(intent)
// 在目标应用界面更新悬浮窗倒计时
if (needCountDown) {
EventBus.getDefault().post(ApplicationEvent.StartCountdownTime(false))
}
} else {
Log.w("Ex-Context", "openApplication: 未找到目标应用的 Launcher Activity,包名:$targetApp")
EventBus.getDefault().post(ApplicationEvent.StopDailyTask)
}
}
fun Context.openApplication() {
val targetApp = Constant.getTargetApp()
if (!isApplicationExist(targetApp)) {
return
}
// 跳转目标应用
val intent = Intent(Intent.ACTION_MAIN, null).apply {
addCategory(Intent.CATEGORY_LAUNCHER)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
setPackage(targetApp)
}
val activities = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(0))
} else {
packageManager.queryIntentActivities(intent, 0)
}
if (activities.isNotEmpty()) {
val info = activities.first()
intent.component = ComponentName(info.activityInfo.packageName, info.activityInfo.name)
startActivity(intent)
// 在目标应用界面更新悬浮窗倒计时
EventBus.getDefault().post(ApplicationEvent.StartCountdownTime(true))
} else {
Log.w("Ex-Context", "openApplication: 未找到目标应用的 Launcher Activity,包名:$targetApp")
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/extensions/DailyTaskBean.kt
================================================
package com.pengxh.daily.app.extensions
import com.github.gzuliyujiang.wheelpicker.entity.TimeEntity
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean
import com.pengxh.daily.app.utils.Constant
import com.pengxh.daily.app.utils.TimeKit
import com.pengxh.kt.lite.extensions.appendZero
import com.pengxh.kt.lite.utils.SaveKeyValues
import java.text.SimpleDateFormat
import java.util.Locale
fun DailyTaskBean.convertToTimeEntity(): TimeEntity {
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
val date = dateFormat.parse("${TimeKit.getTodayDate()} ${this.time}")!!
return TimeEntity.target(date)
}
fun DailyTaskBean.diffCurrent(): Pair {
val needRandom = SaveKeyValues.getValue(Constant.RANDOM_TIME_KEY, true) as Boolean
//18:00:59
val array = this.time.split(":")
var totalSeconds = array[0].toInt() * 3600 + array[1].toInt() * 60 + array[2].toInt()
// 随机时间
if (needRandom) {
val minuteRange = SaveKeyValues.getValue(Constant.RANDOM_MINUTE_RANGE_KEY, 5) as Int
val seedMinute = if (minuteRange > 0) (0 until minuteRange).random() else 0
val seedSeconds = (0 until 60).random() // [0,60)
totalSeconds += seedMinute * 60 + seedSeconds
// 确保不超过当天23:59:59(86399秒)
totalSeconds = minOf(totalSeconds, 86399)
}
// 转换回 时:分:秒 格式
val hour = totalSeconds / 3600
val minute = (totalSeconds % 3600) / 60
val second = totalSeconds % 60
val newTime = "${hour.appendZero()}:${minute.appendZero()}:${second.appendZero()}"
//获取当前日期,计算时间差
val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
val taskDateTime = "${TimeKit.getTodayDate()} $newTime"
val taskDate = simpleDateFormat.parse(taskDateTime) ?: return Pair(newTime, 0)
val currentMillis = System.currentTimeMillis()
val diffSeconds = (taskDate.time - currentMillis) / 1000
return Pair(newTime, diffSeconds.toInt())
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/extensions/Int.kt
================================================
package com.pengxh.daily.app.extensions
import java.util.Locale
fun Int.formatTime(): String {
val total = this.coerceAtLeast(0) // 负数兜底,防止 UI 展示异常
val hours = total / 3600
val minutes = (total % 3600) / 60
val secs = total % 60
return String.format(Locale.getDefault(), "%02d小时%02d分钟%02d秒", hours, minutes, secs)
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/extensions/List.kt
================================================
package com.pengxh.daily.app.extensions
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean
import com.pengxh.daily.app.utils.TimeKit
import java.text.SimpleDateFormat
import java.util.Locale
/**
* 找出任务中,第一个时间晚于当前时间的任务Index
* */
fun List.getTaskIndex(): Int {
val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)
val currentMillis = System.currentTimeMillis()
for ((index, task) in this.withIndex()) {
//获取当前日期,拼给任务时间,不然不好计算时间差
val taskTime = "${TimeKit.getTodayDate()} ${task.time}"
val taskDate = timeFormat.parse(taskTime) ?: continue
// 如果任务时间晚于当前时间,则返回该任务的索引
if (taskDate.time > currentMillis) {
return index
}
}
return -1
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/extensions/String.kt
================================================
package com.pengxh.daily.app.extensions
import com.google.gson.Gson
import com.google.gson.JsonObject
val gson by lazy { Gson() }
/**
* String扩展方法
*/
fun String.getResponseHeader(): Pair {
if (this.isBlank()) {
return Pair(404, "Invalid Response")
}
val jsonObject = gson.fromJson(this, JsonObject::class.java)
val code = jsonObject.get("errcode").asInt
val message = jsonObject.get("errmsg").asString
return Pair(code, message)
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/extensions/View.kt
================================================
package com.pengxh.daily.app.extensions
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
fun View.expand(duration: Long = 350) {
// 先测量真实高度
measure(
View.MeasureSpec.makeMeasureSpec((parent as View).width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
val targetHeight = measuredHeight
layoutParams.height = 0
visibility = View.VISIBLE
alpha = 0f
ValueAnimator.ofInt(0, targetHeight).apply {
this.duration = duration
interpolator = DecelerateInterpolator()
addUpdateListener { animator ->
layoutParams.height = animator.animatedValue as Int
requestLayout()
alpha = animator.animatedFraction
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
alpha = 1f
}
})
start()
}
}
fun View.collapse(duration: Long = 350) {
val initialHeight = measuredHeight
ValueAnimator.ofInt(initialHeight, 0).apply {
this.duration = duration
interpolator = DecelerateInterpolator()
addUpdateListener { animator ->
layoutParams.height = animator.animatedValue as Int
requestLayout()
alpha = 1f - animator.animatedFraction
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
visibility = View.GONE
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
alpha = 1f
}
})
start()
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/model/ExportDataModel.java
================================================
package com.pengxh.daily.app.model;
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean;
import com.pengxh.daily.app.sqlite.bean.EmailConfigBean;
import java.util.List;
/**
* 导出数据模型
*/
public class ExportDataModel {
private List tasks; // 任务列表
private String messageTitle; // 打卡消息标题
private String wxKey; // 企业微信消息Key
private EmailConfigBean emailConfig; // 邮箱配置
private boolean detectGesture; // 检测手势
private boolean backToHome; // 返回桌面
private int resetTime; // 重置时间
private int overTime; // 超时时间
private String command; // 口令
private boolean autoStart; // 自动启动
private boolean randomTime; // 随机时间
private int timeRange; // 时间范围
public List getTasks() {
return tasks;
}
public void setTasks(List tasks) {
this.tasks = tasks;
}
public String getMessageTitle() {
return messageTitle;
}
public void setMessageTitle(String messageTitle) {
this.messageTitle = messageTitle;
}
public String getWxKey() {
return wxKey;
}
public void setWxKey(String wxKey) {
this.wxKey = wxKey;
}
public EmailConfigBean getEmailConfig() {
return emailConfig;
}
public void setEmailConfig(EmailConfigBean emailConfig) {
this.emailConfig = emailConfig;
}
public boolean isDetectGesture() {
return detectGesture;
}
public void setDetectGesture(boolean detectGesture) {
this.detectGesture = detectGesture;
}
public boolean isBackToHome() {
return backToHome;
}
public void setBackToHome(boolean backToHome) {
this.backToHome = backToHome;
}
public int getResetTime() {
return resetTime;
}
public void setResetTime(int resetTime) {
this.resetTime = resetTime;
}
public int getOverTime() {
return overTime;
}
public void setOverTime(int overTime) {
this.overTime = overTime;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public boolean isAutoStart() {
return autoStart;
}
public void setAutoStart(boolean autoStart) {
this.autoStart = autoStart;
}
public boolean isRandomTime() {
return randomTime;
}
public void setRandomTime(boolean randomTime) {
this.randomTime = randomTime;
}
public int getTimeRange() {
return timeRange;
}
public void setTimeRange(int timeRange) {
this.timeRange = timeRange;
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/model/QuestionAnAnswerModel.java
================================================
package com.pengxh.daily.app.model;
public class QuestionAnAnswerModel {
private String question;
private String answer;
public String getQuestion() {
return question;
}
public void setQuestion(String question) {
this.question = question;
}
public String getAnswer() {
return answer;
}
public void setAnswer(String answer) {
this.answer = answer;
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/retrofit/RetrofitService.kt
================================================
package com.pengxh.daily.app.retrofit
import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.POST
import retrofit2.http.QueryMap
interface RetrofitService {
/**
* 企业微信推送消息
* 单机器人每分钟最多 20 条消息,超过限制后立即返回错误码 45009
*/
@POST("/cgi-bin/webhook/send")
suspend fun sendMessage(
@Body requestBody: RequestBody,
@QueryMap map: Map
): String
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/retrofit/RetrofitServiceManager.kt
================================================
package com.pengxh.daily.app.retrofit
import com.pengxh.daily.app.utils.Constant
import com.pengxh.kt.lite.utils.RetrofitFactory
import com.pengxh.kt.lite.utils.SaveKeyValues
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.io.File
import java.security.MessageDigest
import java.util.Base64
object RetrofitServiceManager {
private val api by lazy {
RetrofitFactory.createRetrofit(Constant.WX_WEB_HOOK_URL)
}
suspend fun sendMessage(content: String): String {
val jsonBody = JSONObject().apply {
put("msgtype", "text")
put("text", JSONObject().apply {
put("content", content)
})
}
val requestBody = jsonBody.toString()
.toRequestBody("application/json; charset=utf-8".toMediaType())
val keyMap = HashMap()
keyMap["key"] = SaveKeyValues.getValue(Constant.WX_WEB_HOOK_KEY, "") as String
return api.sendMessage(requestBody, keyMap)
}
suspend fun sendImageMessage(imagePath: String): String {
val imageBytes = File(imagePath).readBytes()
// 计算 Base64
val base64 = Base64.getEncoder().encodeToString(imageBytes)
// 计算文件的MD5
val md5Hash = MessageDigest.getInstance("MD5").digest(imageBytes)
val md5 = md5Hash.joinToString("") { "%02x".format(it) }
val jsonBody = JSONObject().apply {
put("msgtype", "image")
put("image", JSONObject().apply {
put("base64", base64)
put("md5", md5)
})
}
val requestBody = jsonBody.toString()
.toRequestBody("application/json; charset=utf-8".toMediaType())
val keyMap = HashMap()
keyMap["key"] = SaveKeyValues.getValue(Constant.WX_WEB_HOOK_KEY, "") as String
return api.sendMessage(requestBody, keyMap)
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/service/CaptureImageService.kt
================================================
package com.pengxh.daily.app.service
import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.Image
import android.media.ImageReader
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.IBinder
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.graphics.createBitmap
import com.pengxh.daily.app.R
import com.pengxh.daily.app.utils.ApplicationEvent
import com.pengxh.daily.app.utils.Constant
import com.pengxh.daily.app.utils.EmailManager
import com.pengxh.daily.app.utils.HttpRequestManager
import com.pengxh.daily.app.utils.ProjectionSession
import com.pengxh.kt.lite.extensions.createImageFileDir
import com.pengxh.kt.lite.extensions.saveImage
import com.pengxh.kt.lite.utils.SaveKeyValues
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.coroutines.resume
class CaptureImageService : Service(), CoroutineScope by MainScope() {
private val kTag = "CaptureImageService"
private val notificationManager by lazy { getSystemService(NotificationManager::class.java) }
private val notificationBuilder by lazy {
NotificationCompat.Builder(this, "capture_image_service_channel").apply {
setSmallIcon(R.mipmap.ic_launcher)
setContentText("截屏服务已就绪")
setPriority(NotificationCompat.PRIORITY_LOW)
setOngoing(true)
setOnlyAlertOnce(true)
setSilent(true)
setCategory(NotificationCompat.CATEGORY_SERVICE)
setShowWhen(true)
setSound(null)
setVibrate(null)
}
}
private val dateTimeFormat by lazy { SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA) }
private val httpRequestManager by lazy { HttpRequestManager(this) }
private val emailManager by lazy { EmailManager(this) }
private val mpr by lazy { getSystemService(MediaProjectionManager::class.java) }
override fun onCreate() {
super.onCreate()
val name = "${resources.getString(R.string.app_name)}截屏服务"
val channel = NotificationChannel(
"capture_image_service_channel", name, NotificationManager.IMPORTANCE_LOW
).apply {
description = "Channel for Capture Image Service"
}
notificationManager.createNotificationChannel(channel)
val notification = notificationBuilder.build()
// 初始化图片文件目录
createImageFileDir()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
Constant.CAPTURE_IMAGE_SERVICE_NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
)
} else {
startForeground(Constant.CAPTURE_IMAGE_SERVICE_NOTIFICATION_ID, notification)
}
EventBus.getDefault().register(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val resultCode = intent?.getIntExtra("resultCode", Activity.RESULT_CANCELED)
?: return START_STICKY
// resultCode 为 RESULT_CANCELED 说明是服务重启(非用户授权触发),直接返回
if (resultCode == Activity.RESULT_CANCELED) return START_STICKY
val data: Intent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("data", Intent::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra("data")
}
if (data == null) {
Log.w(kTag, "onStartCommand: intent data is null")
EventBus.getDefault().post(ApplicationEvent.ProjectionFailed)
return START_STICKY
}
try {
val projection = mpr.getMediaProjection(resultCode, data)
if (projection == null) {
Log.w(kTag, "getMediaProjection returned null")
EventBus.getDefault().post(ApplicationEvent.ProjectionFailed)
return START_STICKY
}
projection.registerCallback(object : MediaProjection.Callback() {
override fun onStop() {
super.onStop()
ProjectionSession.markStoppedNeedAuth()
}
}, null)
ProjectionSession.setProjection(projection)
Log.d(kTag, "MediaProjection created successfully")
EventBus.getDefault().post(ApplicationEvent.ProjectionReady)
} catch (e: Exception) {
Log.w(kTag, "createMediaProjection failed: ${e.message}", e)
EventBus.getDefault().post(ApplicationEvent.ProjectionFailed)
}
return START_STICKY
}
@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun handleApplicationEvent(event: ApplicationEvent) {
if (event is ApplicationEvent.CaptureScreen) {
captureScreen()
}
}
private fun captureScreen() {
if (ProjectionSession.state != ProjectionSession.State.ACTIVE) {
sendChannelMessage("MediaProjection not active. state=${ProjectionSession.state}")
return
}
val projection = ProjectionSession.getProjection()
if (projection == null) {
sendChannelMessage("MediaProjection not available")
return
}
val metrics = resources.displayMetrics
val width = metrics.widthPixels
val height = metrics.heightPixels
val densityDpi = metrics.densityDpi
val imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
val flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR or
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
launch {
var virtualDisplay: VirtualDisplay? = null
try {
virtualDisplay = projection.createVirtualDisplay(
"CaptureImageDisplay",
width,
height,
densityDpi,
flags,
imageReader.surface,
null,
null
)
// 最多等待2秒
val image = withTimeoutOrNull(2000) {
waitForImageAvailable(imageReader)
}
if (image == null) {
sendChannelMessage("获取图像失败: acquireNextImage返回null")
return@launch
}
val width = image.width
val height = image.height
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * width
val bitmap = createBitmap(width + rowPadding / pixelStride, height)
bitmap.copyPixelsFromBuffer(buffer)
image.close()
val cropped = if (rowPadding != 0) {
Bitmap.createBitmap(bitmap, 0, 0, width, height)
} else bitmap
// 只取中间那部分截图
val y = (cropped.height * 0.2f).toInt()
val halfHeight = y + cropped.height / 2
val topHalf = Bitmap.createBitmap(cropped, 0, y, cropped.width, halfHeight)
val imagePath = "${createImageFileDir()}/${dateTimeFormat.format(Date())}.png"
topHalf.saveImage(imagePath)
EventBus.getDefault().post(ApplicationEvent.CaptureCompleted(imagePath))
} catch (_: RemoteException) {
ProjectionSession.markStoppedNeedAuth()
EventBus.getDefault().post(ApplicationEvent.ProjectionFailed)
} catch (_: SecurityException) {
ProjectionSession.markStoppedNeedAuth()
EventBus.getDefault().post(ApplicationEvent.ProjectionFailed)
} catch (e: Exception) {
sendChannelMessage("截屏失败: ${e.message}")
} finally {
runCatching { virtualDisplay?.release() }
runCatching { imageReader.close() }
}
}
}
private suspend fun waitForImageAvailable(imageReader: ImageReader): Image? {
return suspendCancellableCoroutine { continuation ->
val listener = ImageReader.OnImageAvailableListener { reader ->
val image = reader.acquireLatestImage()
if (image != null) {
continuation.resume(image)
imageReader.setOnImageAvailableListener(null, null)
}
}
continuation.invokeOnCancellation {
imageReader.setOnImageAvailableListener(null, null)
}
imageReader.setOnImageAvailableListener(listener, null)
}
}
private fun sendChannelMessage(content: String) {
val type = SaveKeyValues.getValue(Constant.CHANNEL_TYPE_KEY, -1) as Int
when (type) {
0 -> httpRequestManager.sendMessage("截屏失败", content)
1 -> emailManager.sendEmail("截屏失败", content, false)
else -> Log.w(kTag, "消息渠道不支持: content => $content")
}
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
cancel()
ProjectionSession.clear()
stopForeground(STOP_FOREGROUND_REMOVE)
}
override fun onBind(p0: Intent?): IBinder? {
return null
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/service/CountDownTimerService.kt
================================================
package com.pengxh.daily.app.service
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.CountDownTimer
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import com.pengxh.daily.app.R
import com.pengxh.daily.app.extensions.formatTime
import com.pengxh.daily.app.extensions.openApplication
import com.pengxh.daily.app.utils.Constant
import com.pengxh.daily.app.utils.LogFileManager
/**
* APP倒计时服务,解决手机灭屏后倒计时会出现延迟的问题
* */
class CountDownTimerService : Service() {
private val kTag = "CountDownTimerService"
private val binder by lazy { LocaleBinder() }
private val notificationManager by lazy { getSystemService(NOTIFICATION_SERVICE) as NotificationManager }
private val notificationBuilder by lazy {
NotificationCompat.Builder(this, "countdown_timer_service_channel").apply {
setSmallIcon(R.mipmap.ic_launcher)
setContentText("倒计时服务已就绪")
setPriority(NotificationCompat.PRIORITY_LOW)
setOngoing(true)
setOnlyAlertOnce(true)
setSilent(true)
setCategory(NotificationCompat.CATEGORY_SERVICE)
setShowWhen(true)
setSound(null)
setVibrate(null)
}
}
private val timerLock = Any()
private var countDownTimer: CountDownTimer? = null
@Volatile
private var isTimerRunning = false
private var currentTaskIndex: Int = -1
inner class LocaleBinder : Binder() {
fun getService(): CountDownTimerService = this@CountDownTimerService
}
override fun onBind(intent: Intent?): IBinder {
return binder
}
override fun onCreate() {
super.onCreate()
val name = "${resources.getString(R.string.app_name)}倒计时服务"
val channel = NotificationChannel(
"countdown_timer_service_channel", name, NotificationManager.IMPORTANCE_LOW
).apply {
description = "Channel for CountDownTimer Service"
}
notificationManager.createNotificationChannel(channel)
val notification = notificationBuilder.build()
startForeground(Constant.COUNTDOWN_TIMER_SERVICE_NOTIFICATION_ID, notification)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
fun startCountDown(taskIndex: Int, seconds: Int) {
synchronized(timerLock) {
// 如果是同一个任务正在执行,直接跳过
if (isTimerRunning && currentTaskIndex == taskIndex) {
LogFileManager.writeLog("startCountDown: 任务$taskIndex 已在执行中,跳过")
return@synchronized
}
// 如果有其他任务正在执行,先取消它
if (isTimerRunning) {
countDownTimer?.cancel()
countDownTimer = null
isTimerRunning = false
LogFileManager.writeLog("startCountDown: 取消之前的任务(任务${currentTaskIndex}),准备执行任务$taskIndex")
}
currentTaskIndex = taskIndex
LogFileManager.writeLog("startCountDown: 倒计时任务开始,执行第${taskIndex}个任务")
countDownTimer = object : CountDownTimer(seconds * 1000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
val seconds = (millisUntilFinished / 1000).toInt()
val notification = notificationBuilder.apply {
setContentText("${seconds.formatTime()}后执行第${taskIndex}个任务")
}.build()
notificationManager.notify(
Constant.COUNTDOWN_TIMER_SERVICE_NOTIFICATION_ID,
notification
)
}
override fun onFinish() {
synchronized(timerLock) {
isTimerRunning = false
currentTaskIndex = -1
}
openApplication(true)
}
}.apply {
start()
}
isTimerRunning = true
}
}
fun updateDailyTaskState() {
val notification = notificationBuilder.apply {
setContentText("当天所有任务已执行完毕")
}.build()
notificationManager.notify(Constant.COUNTDOWN_TIMER_SERVICE_NOTIFICATION_ID, notification)
isTimerRunning = false
}
fun cancelCountDown() {
synchronized(timerLock) {
if (isTimerRunning) {
countDownTimer?.cancel()
countDownTimer = null
val notification = notificationBuilder.apply {
setContentText("倒计时任务已停止")
}.build()
notificationManager.notify(
Constant.COUNTDOWN_TIMER_SERVICE_NOTIFICATION_ID,
notification
)
isTimerRunning = false
currentTaskIndex = -1
}
LogFileManager.writeLog("cancelCountDown: 倒计时任务取消")
}
}
override fun onDestroy() {
super.onDestroy()
cancelCountDown()
stopForeground(STOP_FOREGROUND_REMOVE)
Log.d(kTag, "onDestroy: CountDownTimerService")
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/service/FloatingWindowService.kt
================================================
package com.pengxh.daily.app.service
import android.annotation.SuppressLint
import android.app.Service
import android.content.Intent
import android.graphics.PixelFormat
import android.os.IBinder
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import com.pengxh.daily.app.databinding.WindowFloatingBinding
import com.pengxh.daily.app.utils.ApplicationEvent
import com.pengxh.daily.app.utils.Constant
import com.pengxh.kt.lite.utils.SaveKeyValues
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class FloatingWindowService : Service() {
private val kTag = "FloatingWindowService"
private val windowManager by lazy { getSystemService(WindowManager::class.java) }
private lateinit var binding: WindowFloatingBinding
private var floatViewParams: WindowManager.LayoutParams? = null
private var initialX = 0
private var initialY = 0
private var initialTouchX = 0f
private var initialTouchY = 0f
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
binding = WindowFloatingBinding.inflate(LayoutInflater.from(this))
EventBus.getDefault().register(this)
floatViewParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.CENTER or Gravity.TOP
}.also {
windowManager.addView(binding.root, it)
}
// 获取目标应用任务超时时间
val time = SaveKeyValues.getValue(
Constant.STAY_DD_TIMEOUT_KEY, Constant.DEFAULT_OVER_TIME
) as Int
binding.timeView.text = "${time}s"
// 移动悬浮窗
onDragMove()
}
@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun handleApplicationEvent(event: ApplicationEvent) {
when (event) {
is ApplicationEvent.ShowFloatingWindow -> {
binding.root.alpha = 1.0f
val time = SaveKeyValues.getValue(
Constant.STAY_DD_TIMEOUT_KEY, Constant.DEFAULT_OVER_TIME
) as Int
binding.timeView.text = "${time}s"
}
is ApplicationEvent.HideFloatingWindow -> {
binding.root.alpha = 0.0f
binding.timeView.text = "0s"
}
is ApplicationEvent.SetTaskOvertime -> {
// 更新目标应用任务超时时间
binding.timeView.text = "${event.time}s"
}
is ApplicationEvent.UpdateFloatingViewTime -> {
// 更新悬浮窗倒计时
binding.timeView.text = "${event.tick}s"
if (event.tick < 1) {
binding.root.alpha = 0.0f
} else {
binding.root.alpha = 1.0f
}
}
else -> {}
}
}
@SuppressLint("ClickableViewAccessibility")
private fun onDragMove() {
binding.root.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
event ?: return false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
initialX = floatViewParams?.x ?: 0
initialY = floatViewParams?.y ?: 0
initialTouchX = event.rawX
initialTouchY = event.rawY
return true
}
MotionEvent.ACTION_MOVE -> {
floatViewParams?.let {
it.x = initialX + (event.rawX - initialTouchX).toInt()
it.y = initialY + (event.rawY - initialTouchY).toInt()
windowManager.updateViewLayout(binding.root, it)
}
return true
}
else -> return false
}
}
})
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
if (::binding.isInitialized && binding.root.isAttachedToWindow) {
try {
windowManager.removeViewImmediate(binding.root)
} catch (e: IllegalArgumentException) {
Log.w(kTag, "View not attached to window manager", e)
}
}
Log.d(kTag, "onDestroy: FloatingWindowService")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/service/ForegroundRunningService.kt
================================================
package com.pengxh.daily.app.service
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.pengxh.daily.app.R
import com.pengxh.daily.app.utils.AlarmScheduler
import com.pengxh.daily.app.utils.ApplicationEvent
import com.pengxh.daily.app.utils.Constant
import com.pengxh.kt.lite.utils.SaveKeyValues
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.Calendar
import java.util.Locale
/**
* APP前台服务,降低APP被系统杀死的可能性
* */
class ForegroundRunningService : Service() {
override fun onCreate() {
super.onCreate()
val notificationManager = getSystemService(NotificationManager::class.java)
val name = "${resources.getString(R.string.app_name)}前台服务"
val channel = NotificationChannel(
"foreground_running_service_channel", name, NotificationManager.IMPORTANCE_LOW
).apply {
description = "Channel for Foreground Running Service"
}
notificationManager.createNotificationChannel(channel)
val notificationBuilder =
NotificationCompat.Builder(this, "foreground_running_service_channel").apply {
setSmallIcon(R.mipmap.ic_launcher)
setContentText("为保证程序正常运行,请勿移除此通知")
setPriority(NotificationCompat.PRIORITY_LOW) // 设置通知优先级
setOngoing(true)
setOnlyAlertOnce(true)
setSilent(true)
setCategory(NotificationCompat.CATEGORY_SERVICE)
setShowWhen(true)
setSound(null) // 禁用声音
setVibrate(null) // 禁用振动
}
val notification = notificationBuilder.build()
startForeground(Constant.FOREGROUND_RUNNING_SERVICE_NOTIFICATION_ID, notification)
val filter = IntentFilter(Intent.ACTION_TIME_TICK)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(timeTickReceiver, filter, RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(timeTickReceiver, filter)
}
EventBus.getDefault().register(this)
// 立即更新一次倒计时显示
updateResetTimeView()
// 每次 Service 启动时重新注册 Alarm
val resetHour = SaveKeyValues.getValue(
Constant.RESET_TIME_KEY, Constant.DEFAULT_RESET_HOUR
) as Int
AlarmScheduler.schedule(this, resetHour)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
private val timeTickReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
intent?.action?.let {
if (it == Intent.ACTION_TIME_TICK) {
// 仅更新倒计时显示,重置任务由 AlarmManager 负责
updateResetTimeView()
}
}
}
}
@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun handleApplicationEvent(event: ApplicationEvent) {
if (event is ApplicationEvent.SetResetTaskTime) {
// 重新计算并更新倒计时显示
updateResetTimeView()
}
}
private fun updateResetTimeView() {
val resetHour = SaveKeyValues.getValue(
Constant.RESET_TIME_KEY, Constant.DEFAULT_RESET_HOUR
) as Int
val seconds = resetTaskSeconds(resetHour)
val hours = seconds / 3600
val minutes = (seconds % 3600) / 60
val time = String.format(Locale.getDefault(), "%02d小时%02d分钟", hours, minutes)
EventBus.getDefault().post(ApplicationEvent.UpdateResetTickTime("${time}后刷新每日任务"))
}
private fun resetTaskSeconds(hour: Int): Int {
val calendar = Calendar.getInstance()
val currentHour = calendar.get(Calendar.HOUR_OF_DAY)
val currentMinute = calendar.get(Calendar.MINUTE)
val currentSecond = calendar.get(Calendar.SECOND)
// 设置今天的计划时间
val todayTargetMillis = calendar.clone() as Calendar
todayTargetMillis.set(Calendar.HOUR_OF_DAY, hour)
todayTargetMillis.set(Calendar.MINUTE, 0)
todayTargetMillis.set(Calendar.SECOND, 0)
todayTargetMillis.set(Calendar.MILLISECOND, 0)
// 根据当前时间决定计算哪一天的计划时间
val targetMillis = if (currentHour < hour) {
// 今天还没到计划时间
todayTargetMillis.timeInMillis
} else if (currentHour == hour && currentMinute == 0 && currentSecond == 0) {
// 刚好是整点,计算明天的
todayTargetMillis.add(Calendar.DATE, 1)
todayTargetMillis.timeInMillis
} else {
// 今天已经过了计划时间,计算明天的
todayTargetMillis.add(Calendar.DATE, 1)
todayTargetMillis.timeInMillis
}
val delta = (targetMillis - System.currentTimeMillis()) / 1000
return delta.toInt()
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
try {
unregisterReceiver(timeTickReceiver)
} catch (e: Exception) {
e.printStackTrace()
}
stopForeground(STOP_FOREGROUND_REMOVE)
}
override fun onBind(intent: Intent?): IBinder? = null
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/service/NotificationMonitorService.kt
================================================
package com.pengxh.daily.app.service
import android.app.Notification
import android.content.ComponentName
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import android.util.Log
import com.pengxh.daily.app.extensions.openApplication
import com.pengxh.daily.app.sqlite.DatabaseWrapper
import com.pengxh.daily.app.sqlite.bean.NotificationBean
import com.pengxh.daily.app.ui.MainActivity
import com.pengxh.daily.app.utils.ApplicationEvent
import com.pengxh.daily.app.utils.Constant
import com.pengxh.daily.app.utils.EmailManager
import com.pengxh.daily.app.utils.HttpRequestManager
import com.pengxh.daily.app.utils.ProjectionSession
import com.pengxh.kt.lite.extensions.show
import com.pengxh.kt.lite.extensions.timestampToCompleteDate
import com.pengxh.kt.lite.utils.SaveKeyValues
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
/**
* @description: 状态栏监听服务
* @author: Pengxh
* @email: 290677893@qq.com
* @date: 2019/12/25 23:17
*/
class NotificationMonitorService : NotificationListenerService() {
private val kTag = "MonitorService"
private val httpRequestManager by lazy { HttpRequestManager(this) }
private val emailManager by lazy { EmailManager(this) }
private val auxiliaryApp = arrayOf(Constant.WECHAT, Constant.QQ, Constant.TIM, Constant.ZFB)
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var listenerConnected = false
/**
* 有可用的并且和通知管理器连接成功时回调
*/
override fun onListenerConnected() {
listenerConnected = true
EventBus.getDefault().post(ApplicationEvent.ListenerConnected)
}
/**
* 当有新通知到来时会回调
*/
override fun onNotificationPosted(sbn: StatusBarNotification) {
val extras = sbn.notification.extras
val pkg = sbn.packageName
val title = extras.getString(Notification.EXTRA_TITLE) ?: ""
val notice = extras.getString(Notification.EXTRA_TEXT)
?: extras.getString(Notification.EXTRA_BIG_TEXT)
?: extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES)
?.joinToString("\n")
?: extras.getString(Notification.EXTRA_SUMMARY_TEXT)
if (notice.isNullOrBlank()) {
return
}
val targetApp = Constant.getTargetApp()
// 保存指定包名的通知,其他的一律不保存
saveTargetNotice(pkg, targetApp, title, notice)
// 目标应用打卡通知
val resultSource = SaveKeyValues.getValue(Constant.RESULT_SOURCE_KEY, 0) as Int
if (resultSource == 0) {
if (pkg == targetApp && notice.contains("成功")) {
EventBus.getDefault().post(ApplicationEvent.GoBackMainActivity)
"即将发送通知邮件,请注意查收".show(this)
val messageTitle =
SaveKeyValues.getValue(Constant.MESSAGE_TITLE_KEY, "打卡结果通知") as String
sendChannelMessage(title.ifBlank { messageTitle }, notice)
}
}
// 其他消息指令
handleRemoteCommand(pkg, notice)
}
private fun saveTargetNotice(pkg: String, targetApp: String, title: String, notice: String) {
if (pkg != targetApp && pkg !in auxiliaryApp) return
NotificationBean().apply {
packageName = pkg
noticeTitle = title
noticeMessage = notice
postTime = System.currentTimeMillis().timestampToCompleteDate()
}.also {
serviceScope.launch {
try {
DatabaseWrapper.insertNotice(it)
} catch (e: Exception) {
Log.e(kTag, "Insert notice failed", e)
}
}
}
}
private fun handleRemoteCommand(pkg: String, notice: String) {
if (pkg in auxiliaryApp) {
when {
notice.contains("启动") -> {
EventBus.getDefault().post(ApplicationEvent.StartDailyTask)
}
notice.contains("停止") -> {
EventBus.getDefault().post(ApplicationEvent.StopDailyTask)
}
notice.contains("开始循环") -> {
SaveKeyValues.putValue(Constant.TASK_AUTO_START_KEY, true)
sendChannelMessage("循环任务状态通知", "循环任务状态已更新为:开启")
}
notice.contains("暂停循环") -> {
SaveKeyValues.putValue(Constant.TASK_AUTO_START_KEY, false)
sendChannelMessage("循环任务状态通知", "循环任务状态已更新为:暂停")
}
notice.contains("息屏") -> {
EventBus.getDefault().post(ApplicationEvent.ShowMaskView)
}
notice.contains("亮屏") -> {
EventBus.getDefault().post(ApplicationEvent.HideMaskView)
}
notice.contains("考勤记录") -> {
serviceScope.launch {
val notices = try {
DatabaseWrapper.loadCurrentDayNotice()
} catch (e: Exception) {
Log.e(kTag, "Load notices failed", e)
emptyList()
}
val record = buildString {
var index = 1
notices.filter {
it.noticeMessage.contains("考勤打卡")
}.forEach {
append("【第${index}次】${it.noticeMessage},时间:${it.postTime}\r\n")
index++
}
}
withContext(Dispatchers.Main) {
sendChannelMessage("当天考勤记录通知", record)
}
}
}
notice.contains("状态查询") -> {
val type = SaveKeyValues.getValue(Constant.CHANNEL_TYPE_KEY, -1) as Int
val content = buildString {
appendLine("任务状态:${if (MainActivity.isTaskStarted) "运行中" else "已停止"}")
appendLine("通知监听:${if (listenerConnected) "正常" else "断开"}")
appendLine("截图服务:${if (ProjectionSession.state == ProjectionSession.State.ACTIVE) "正常" else "断开"}")
append("消息渠道:${if (type == 0) "企业微信" else "QQ邮箱"}")
}
sendChannelMessage("状态查询通知", content)
}
notice.contains("截屏") -> {
if (ProjectionSession.state == ProjectionSession.State.ACTIVE) {
openApplication()
} else {
sendChannelMessage("截屏状态通知", "截屏服务已断开,截屏失败")
}
}
else -> {
val key = SaveKeyValues.getValue(Constant.TASK_COMMAND_KEY, "打卡") as String
if (notice.contains(key)) {
openApplication(true)
}
}
}
}
}
private fun sendChannelMessage(title: String, content: String) {
val type = SaveKeyValues.getValue(Constant.CHANNEL_TYPE_KEY, -1) as Int
when (type) {
0 -> {
// 企业微信
httpRequestManager.sendMessage(title, content)
}
1 -> {
// QQ邮箱
emailManager.sendEmail(title, content, false)
}
else -> {
Log.d(kTag, "sendChannelMessage: 消息渠道不支持")
}
}
}
/**
* 当有通知移除时会回调
*/
override fun onNotificationRemoved(sbn: StatusBarNotification) {}
override fun onListenerDisconnected() {
listenerConnected = false
EventBus.getDefault().post(ApplicationEvent.ListenerDisconnected)
// 主动请求系统重新绑定监听服务
requestRebind(ComponentName(this, NotificationMonitorService::class.java))
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/sqlite/DailyTaskDataBase.java
================================================
package com.pengxh.daily.app.sqlite;
import androidx.room.Database;
import androidx.room.RoomDatabase;
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean;
import com.pengxh.daily.app.sqlite.bean.EmailConfigBean;
import com.pengxh.daily.app.sqlite.bean.NotificationBean;
import com.pengxh.daily.app.sqlite.dao.DailyTaskBeanDao;
import com.pengxh.daily.app.sqlite.dao.EmailConfigBeanDao;
import com.pengxh.daily.app.sqlite.dao.NotificationBeanDao;
@Database(entities = {DailyTaskBean.class, NotificationBean.class, EmailConfigBean.class}, version = 1)
public abstract class DailyTaskDataBase extends RoomDatabase {
public abstract DailyTaskBeanDao dailyTaskDao();
public abstract NotificationBeanDao noticeDao();
public abstract EmailConfigBeanDao emailConfigDao();
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/sqlite/DatabaseWrapper.kt
================================================
package com.pengxh.daily.app.sqlite
import com.pengxh.daily.app.DailyTaskApplication
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean
import com.pengxh.daily.app.sqlite.bean.EmailConfigBean
import com.pengxh.daily.app.sqlite.bean.NotificationBean
import com.pengxh.daily.app.utils.TimeKit
import com.pengxh.kt.lite.extensions.timestampToCompleteDate
object DatabaseWrapper {
private val dailyTaskDao by lazy { DailyTaskApplication.get().dataBase.dailyTaskDao() }
fun loadAllTask(): ArrayList {
return dailyTaskDao.loadAll() as ArrayList
}
fun isTaskTimeExist(time: String): Boolean {
return dailyTaskDao.queryTaskByTime(time) > 0
}
fun updateTask(bean: DailyTaskBean) {
dailyTaskDao.update(bean)
}
fun deleteTask(bean: DailyTaskBean) {
dailyTaskDao.delete(bean)
}
fun insert(bean: DailyTaskBean) {
dailyTaskDao.insert(bean)
}
/*****************************************************************************************/
private val noticeDao by lazy { DailyTaskApplication.get().dataBase.noticeDao() }
fun deleteAllNotice() {
noticeDao.deleteAll()
}
fun loadWeeklyNotice(startDate: String, endDate: String): MutableList {
return noticeDao.loadWeeklyNotice(startDate, endDate)
}
fun loadCurrentDayNotice(): MutableList {
return noticeDao.loadCurrentDayNotice(TimeKit.getTodayDate())
}
fun insertNotice(bean: NotificationBean) {
noticeDao.insert(bean)
}
/*****************************************************************************************/
private val emailConfigDao by lazy { DailyTaskApplication.get().dataBase.emailConfigDao() }
fun insertConfig(outbox: String, authCode: String, inbox: String) {
val bean = EmailConfigBean()
bean.outbox = outbox
bean.authCode = authCode
bean.inbox = inbox
bean.createTime = System.currentTimeMillis().timestampToCompleteDate()
emailConfigDao.insert(bean)
}
fun loadAll(): List {
return emailConfigDao.loadAll()
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/sqlite/bean/DailyTaskBean.java
================================================
package com.pengxh.daily.app.sqlite.bean;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "daily_task_table")
public class DailyTaskBean {
@PrimaryKey(autoGenerate = true)
private int id;//主键ID
private String time;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/sqlite/bean/EmailConfigBean.java
================================================
package com.pengxh.daily.app.sqlite.bean;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "email_config_table")
public class EmailConfigBean {
@PrimaryKey(autoGenerate = true)
private int id;//主键ID
private String outbox = ""; // 发件箱
private String authCode = ""; // 授权码
private String inbox = ""; // 收件箱
private String createTime = ""; // 时间
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getOutbox() {
return outbox;
}
public void setOutbox(String outbox) {
this.outbox = outbox;
}
public String getAuthCode() {
return authCode;
}
public void setAuthCode(String authCode) {
this.authCode = authCode;
}
public String getInbox() {
return inbox;
}
public void setInbox(String inbox) {
this.inbox = inbox;
}
public String getCreateTime() {
return createTime;
}
public void setCreateTime(String createTime) {
this.createTime = createTime;
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/sqlite/bean/NotificationBean.java
================================================
package com.pengxh.daily.app.sqlite.bean;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "notice_record_table")
public class NotificationBean {
@PrimaryKey(autoGenerate = true)
private int id;//主键ID
private String packageName;
private String noticeTitle;
private String noticeMessage;
private String postTime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getPackageName() {
return packageName;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public String getNoticeTitle() {
return noticeTitle;
}
public void setNoticeTitle(String noticeTitle) {
this.noticeTitle = noticeTitle;
}
public String getNoticeMessage() {
return noticeMessage;
}
public void setNoticeMessage(String noticeMessage) {
this.noticeMessage = noticeMessage;
}
public String getPostTime() {
return postTime;
}
public void setPostTime(String postTime) {
this.postTime = postTime;
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/sqlite/dao/DailyTaskBeanDao.java
================================================
package com.pengxh.daily.app.sqlite.dao;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean;
import java.util.List;
@Dao
public interface DailyTaskBeanDao {
@Query("SELECT * FROM daily_task_table ORDER BY time ASC")
List loadAll();
@Update
void update(DailyTaskBean bean);
@Delete
void delete(DailyTaskBean bean);
@Query("SELECT COUNT(*) FROM daily_task_table WHERE time = :time")
int queryTaskByTime(String time);
@Insert
void insert(DailyTaskBean bean);
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/sqlite/dao/EmailConfigBeanDao.java
================================================
package com.pengxh.daily.app.sqlite.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import com.pengxh.daily.app.sqlite.bean.EmailConfigBean;
import java.util.List;
@Dao
public interface EmailConfigBeanDao {
@Insert
void insert(EmailConfigBean bean);
@Update
void update(EmailConfigBean bean);
@Query("SELECT * FROM email_config_table ORDER BY createTime DESC LIMIT 1")
EmailConfigBean loadEmailConfig();
@Query("SELECT * FROM email_config_table")
List loadAll();
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/sqlite/dao/NotificationBeanDao.java
================================================
package com.pengxh.daily.app.sqlite.dao;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import com.pengxh.daily.app.sqlite.bean.NotificationBean;
import java.util.List;
@Dao
public interface NotificationBeanDao {
@Query("DELETE FROM notice_record_table")
void deleteAll();
@Query("SELECT * FROM notice_record_table WHERE date(postTime) >= date(:startDate) AND date(postTime) <= date(:endDate) ORDER BY postTime DESC")
List loadWeeklyNotice(String startDate, String endDate);
@Query("SELECT * FROM notice_record_table WHERE postTime LIKE :date || '%'")
List loadCurrentDayNotice(String date);
@Insert
void insert(NotificationBean bean);
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/ui/MainActivity.kt
================================================
package com.pengxh.daily.app.ui
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.provider.Settings
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.github.gzuliyujiang.wheelpicker.widget.TimeWheelLayout
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textview.MaterialTextView
import com.pengxh.daily.app.R
import com.pengxh.daily.app.adapter.DailyTaskAdapter
import com.pengxh.daily.app.databinding.ActivityMainBinding
import com.pengxh.daily.app.extensions.convertToTimeEntity
import com.pengxh.daily.app.service.CountDownTimerService
import com.pengxh.daily.app.service.FloatingWindowService
import com.pengxh.daily.app.service.ForegroundRunningService
import com.pengxh.daily.app.sqlite.DatabaseWrapper
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean
import com.pengxh.daily.app.utils.ApplicationEvent
import com.pengxh.daily.app.utils.Constant
import com.pengxh.daily.app.utils.DailyTask
import com.pengxh.daily.app.utils.GestureController
import com.pengxh.daily.app.utils.LogFileManager
import com.pengxh.daily.app.utils.MaskViewController
import com.pengxh.daily.app.utils.MessageDispatcher
import com.pengxh.daily.app.utils.TaskDataManager
import com.pengxh.daily.app.utils.TaskScheduler
import com.pengxh.daily.app.utils.TimeoutTimerManager
import com.pengxh.daily.app.utils.WatermarkDrawable
import com.pengxh.daily.app.vm.MessageViewModel
import com.pengxh.kt.lite.base.KotlinBaseActivity
import com.pengxh.kt.lite.divider.RecyclerViewItemOffsets
import com.pengxh.kt.lite.extensions.convertColor
import com.pengxh.kt.lite.extensions.dp2px
import com.pengxh.kt.lite.extensions.navigatePageTo
import com.pengxh.kt.lite.extensions.show
import com.pengxh.kt.lite.utils.SaveKeyValues
import com.pengxh.kt.lite.widget.dialog.AlertInputDialog
import com.pengxh.kt.lite.widget.dialog.BottomActionSheet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivity : KotlinBaseActivity(), TaskScheduler.TaskStateListener {
companion object {
var isTaskStarted = false
}
private val context = this
private val dateFormat by lazy {
SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEEE", Locale.CHINA)
}
private val marginOffset by lazy { 16.dp2px(this) }
private val permissionContract by lazy { ActivityResultContracts.StartActivityForResult() }
private val taskDataManager by lazy { TaskDataManager() }
private val mainHandler = Handler(Looper.getMainLooper())
private val messageViewModel by lazy { ViewModelProvider(this)[MessageViewModel::class.java] }
private val messageDispatcher by lazy { MessageDispatcher(this, messageViewModel) }
private lateinit var insetsController: WindowInsetsControllerCompat
private lateinit var maskViewController: MaskViewController
private lateinit var gestureController: GestureController
private lateinit var dailyTaskAdapter: DailyTaskAdapter
private lateinit var taskScheduler: TaskScheduler
private lateinit var timeoutTimerManager: TimeoutTimerManager
private var taskBeans = mutableListOf()
private var imagePath = ""
private var hasCaptured = false
override fun observeRequestState() {
}
override fun initViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater)
}
override fun setupTopBarLayout() {
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { view, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
view.setPadding(0, statusBarHeight, 0, 0)
insets
}
// 显示时间
mainHandler.post(object : Runnable {
override fun run() {
val currentTime = dateFormat.format(Date())
val parts = currentTime.split(" ")
binding.toolbar.apply {
title = parts[2]
subtitle = "${parts[0]} ${parts[1]}"
}
mainHandler.postDelayed(this, 1000)
}
})
binding.toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.menu_add_task -> {
if (taskScheduler.isTaskStarted()) {
"任务进行中,无法添加".show(this)
return@setOnMenuItemClickListener true
}
if (taskBeans.isNotEmpty()) {
createTask()
} else {
BottomActionSheet.Builder()
.setContext(this)
.setActionItemTitle(arrayListOf("添加任务", "导入任务"))
.setItemTextColor(R.color.theme_color.convertColor(this))
.setOnActionSheetListener(object :
BottomActionSheet.OnActionSheetListener {
override fun onActionItemClick(position: Int) {
when (position) {
0 -> createTask()
1 -> importTask()
}
}
}).build().show()
}
}
R.id.menu_settings -> {
MaterialAlertDialogBuilder(this)
.setTitle("使用须知")
.setMessage("本软件完全免费!仅供内部使用!严禁商用或者用作其他非法用途!\r\n近期发现有人在咸鱼私自倒卖本软件,请勿购买!如有购买,请联系卖家退款!")
.setCancelable(false) // 禁止点击外部关闭
.setPositiveButton("知道了") { _, _ ->
navigatePageTo()
}.show()
}
}
true
}
}
override fun initOnCreate(savedInstanceState: Bundle?) {
EventBus.getDefault().register(this)
// 显示悬浮窗
if (Settings.canDrawOverlays(this)) {
Intent(this, FloatingWindowService::class.java).apply {
startService(this)
}
} else {
// 悬浮窗权限并显示悬浮窗
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
overlayPermissionLauncher.launch(intent)
}
insetsController = WindowCompat.getInsetsController(window, binding.rootView)
Intent(this, ForegroundRunningService::class.java).apply {
startForegroundService(this)
}
Intent(this, CountDownTimerService::class.java).apply {
bindService(this, serviceConnection, BIND_AUTO_CREATE)
}
val watermark = DailyTask.getWatermarkText()
binding.contentView.background = WatermarkDrawable(this, watermark)
// 数据
taskBeans = DatabaseWrapper.loadAllTask()
if (taskBeans.isEmpty()) {
binding.recyclerView.visibility = View.GONE
binding.emptyView.visibility = View.VISIBLE
} else {
binding.recyclerView.visibility = View.VISIBLE
binding.emptyView.visibility = View.GONE
}
dailyTaskAdapter = DailyTaskAdapter(this, taskBeans)
dailyTaskAdapter.setOnItemClickListener(object : DailyTaskAdapter.OnItemClickListener {
override fun onItemClick(position: Int) {
itemClick(position)
}
override fun onItemLongClick(position: Int) {
itemLongClick(position)
}
})
binding.recyclerView.adapter = dailyTaskAdapter
binding.recyclerView.addItemDecoration(
RecyclerViewItemOffsets(
marginOffset, marginOffset shr 1, marginOffset, marginOffset shr 1
)
)
maskViewController = MaskViewController(this, binding, insetsController)
gestureController = GestureController(this, maskViewController, mainHandler)
taskScheduler = TaskScheduler(mainHandler, taskBeans, this)
timeoutTimerManager = TimeoutTimerManager(mainHandler)
}
@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun handleApplicationEvent(event: ApplicationEvent) {
when (event) {
is ApplicationEvent.ShowMaskView -> {
if (!maskViewController.isMaskVisible()) {
maskViewController.showMaskView(mainHandler)
}
}
is ApplicationEvent.HideMaskView -> {
if (maskViewController.isMaskVisible()) {
maskViewController.hideMaskView(mainHandler)
}
}
is ApplicationEvent.ResetDailyTask -> {
taskScheduler.startTask()
}
is ApplicationEvent.UpdateResetTickTime -> {
binding.repeatTimeView.text = event.countDownTime
}
is ApplicationEvent.StartDailyTask -> {
if (taskScheduler.isTaskStarted()) {
return
}
taskScheduler.startTask()
}
is ApplicationEvent.StopDailyTask -> {
if (!taskScheduler.isTaskStarted()) {
return
}
taskScheduler.stopTask()
}
is ApplicationEvent.GoBackMainActivity -> { // 打卡成功发送的消息,回到主界面
timeoutTimerManager.cancelTimeoutTimer()
backToMainActivity()
taskScheduler.executeNextTask()
}
is ApplicationEvent.StartCountdownTime -> {
if (event.isRemoteCommand) {
imagePath = ""
// 先跳转到目标应用,等待加载,然后截屏
object : CountDownTimer(5000, 1000) {
override fun onTick(millisUntilFinished: Long) {
val tick = (millisUntilFinished / 1000).toInt()
// 更新悬浮窗倒计时
EventBus.getDefault().post(ApplicationEvent.UpdateFloatingViewTime(tick))
if (tick <= 2 && !hasCaptured) {
hasCaptured = true
EventBus.getDefault().post(ApplicationEvent.CaptureScreen)
}
}
override fun onFinish() {
backToMainActivity()
if (imagePath == "") {
messageDispatcher.sendMessage(
"截屏状态通知", "截图完成,但是无法获取截图,请手动查看结果"
)
} else {
messageDispatcher.sendAttachmentMessage(
"截屏状态通知", "截图完成,结果请查看附件", imagePath
)
}
hasCaptured = false
}
}.start()
} else {
timeoutTimerManager.startTimeoutTimer {
backToMainActivity()
val resultSource =
SaveKeyValues.getValue(Constant.RESULT_SOURCE_KEY, 0) as Int
if (resultSource == 0) {
// 如果倒计时结束,那么表明没有收到打卡成功的通知
messageDispatcher.sendMessage("", "")
} else {
if (imagePath == "") {
messageDispatcher.sendMessage(
"", "打卡完成,但是无法获取截图,请手动查看结果"
)
} else {
messageDispatcher.sendAttachmentMessage(
"", "打卡完成,结果请查看附件", imagePath
)
}
}
taskScheduler.executeNextTask()
}
}
}
is ApplicationEvent.CaptureCompleted -> {
imagePath = event.imagePath
}
else -> {}
}
}
private fun backToMainActivity() {
if (SaveKeyValues.getValue(Constant.BACK_TO_HOME_KEY, false) as Boolean) {
//模拟点击Home键
val home = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_HOME)
}
startActivity(home)
lifecycleScope.launch(Dispatchers.IO) {
delay(2000)
withContext(Dispatchers.Main) {
navigatePageTo()
}
}
} else {
navigatePageTo()
}
}
override fun onTaskStarted() {
isTaskStarted = true
binding.executeTaskButton.setIconResource(R.mipmap.ic_stop)
binding.executeTaskButton.setIconTintResource(R.color.red)
binding.executeTaskButton.text = "停止"
messageDispatcher.sendMessage("启动任务通知", "任务启动成功,请注意下次打卡时间")
}
override fun onTaskStopped() {
isTaskStarted = false
// 重置UI状态
dailyTaskAdapter.updateCurrentTaskState(-1)
binding.tipsView.text = ""
// 重置按钮状态
binding.executeTaskButton.setIconResource(R.mipmap.ic_start)
binding.executeTaskButton.setIconTintResource(R.color.ios_green)
binding.executeTaskButton.text = "启动"
messageDispatcher.sendMessage("停止任务通知", "任务停止成功,请及时打开下次任务")
}
override fun onTaskCompleted() {
// 任务全部完成
binding.tipsView.text = "当天所有任务已执行完毕"
binding.tipsView.setTextColor(R.color.ios_green.convertColor(context))
dailyTaskAdapter.updateCurrentTaskState(-1)
messageDispatcher.sendMessage("任务状态通知", "今日任务已全部执行完毕")
}
override fun onTaskExecuting(taskIndex: Int, task: DailyTaskBean, realTime: String) {
// 任务执行中
binding.tipsView.text = String.format(
Locale.getDefault(), "准备执行第 %d 个任务", taskIndex
)
binding.tipsView.setTextColor(R.color.theme_color.convertColor(context))
dailyTaskAdapter.updateCurrentTaskState(taskIndex - 1, realTime)
val content = buildString {
appendLine("准备执行第 $taskIndex 个任务")
appendLine("计划时间:${task.time}")
append("实际时间:$realTime")
}
messageDispatcher.sendMessage("任务执行通知", content)
}
override fun onTaskExecutionError(message: String) {
messageDispatcher.sendMessage("任务执行出错通知", message)
}
private val overlayPermissionLauncher = registerForActivityResult(permissionContract) {
if (Settings.canDrawOverlays(this)) {
Intent(this, FloatingWindowService::class.java).apply {
startService(this)
}
}
}
/**
* 服务绑定
* */
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val binder = service as CountDownTimerService.LocaleBinder
val serviceInstance = binder.getService()
taskScheduler.setCountDownTimerService(serviceInstance)
}
override fun onServiceDisconnected(name: ComponentName?) {
taskScheduler.setCountDownTimerService(null)
}
}
/**
* 列表项单击
* */
private fun itemClick(position: Int) {
if (taskScheduler.isTaskStarted()) {
"任务进行中,无法修改".show(this)
return
}
val item = taskBeans[position]
val view = layoutInflater.inflate(R.layout.bottom_sheet_layout_select_time, null)
val dialog = BottomSheetDialog(this)
dialog.setContentView(view)
val titleView = view.findViewById(R.id.titleView)
titleView.text = "修改任务时间"
val timePicker = view.findViewById(R.id.timePicker)
timePicker.setDefaultValue(item.convertToTimeEntity())
view.findViewById(R.id.saveButton).setOnClickListener {
val time = String.format(
Locale.getDefault(),
"%02d:%02d:%02d",
timePicker.selectedHour,
timePicker.selectedMinute,
timePicker.selectedSecond
)
item.time = time
DatabaseWrapper.updateTask(item)
taskBeans = DatabaseWrapper.loadAllTask()
dailyTaskAdapter.refresh(taskBeans)
dialog.dismiss()
}
dialog.show()
}
/**
* 列表项长按
* */
private fun itemLongClick(position: Int) {
if (taskScheduler.isTaskStarted()) {
"任务进行中,无法删除".show(this)
return
}
MaterialAlertDialogBuilder(this)
.setTitle("删除任务")
.setMessage("确定要删除这个任务吗?")
.setCancelable(false) // 禁止点击外部关闭
.setPositiveButton("确定") { _, _ ->
try {
val item = taskBeans[position]
DatabaseWrapper.deleteTask(item)
// 为了确保数据一致性,重新从数据库加载数据
taskBeans = DatabaseWrapper.loadAllTask()
dailyTaskAdapter.refresh(taskBeans)
if (taskBeans.isEmpty()) {
binding.recyclerView.visibility = View.GONE
binding.emptyView.visibility = View.VISIBLE
} else {
binding.recyclerView.visibility = View.VISIBLE
binding.emptyView.visibility = View.GONE
}
} catch (e: IndexOutOfBoundsException) {
e.printStackTrace()
}
}.setNegativeButton("取消", null).show()
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
ev?.let {
gestureController.onTouchEvent(it)
}
return super.dispatchTouchEvent(ev)
}
override fun initEvent() {
binding.executeTaskButton.setOnClickListener {
if (taskScheduler.isTaskStarted()) {
taskScheduler.stopTask()
} else {
if (DatabaseWrapper.loadAllTask().isEmpty()) {
"循环任务启动失败,请先添加任务时间点".show(this)
return@setOnClickListener
}
taskScheduler.startTask()
}
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
if (maskViewController.isMaskVisible()) {
maskViewController.hideMaskView(mainHandler)
} else {
maskViewController.showMaskView(mainHandler)
}
return true
}
return super.onKeyDown(keyCode, event)
}
private fun createTask() {
val view = layoutInflater.inflate(R.layout.bottom_sheet_layout_select_time, null)
val dialog = BottomSheetDialog(this)
dialog.setContentView(view)
val titleView = view.findViewById(R.id.titleView)
titleView.text = "添加任务"
val timePicker = view.findViewById(R.id.timePicker)
view.findViewById(R.id.saveButton).setOnClickListener {
val time = String.format(
Locale.getDefault(),
"%02d:%02d:%02d",
timePicker.selectedHour,
timePicker.selectedMinute,
timePicker.selectedSecond
)
if (DatabaseWrapper.isTaskTimeExist(time)) {
"任务时间点已存在".show(this)
return@setOnClickListener
}
binding.recyclerView.visibility = View.VISIBLE
binding.emptyView.visibility = View.GONE
val bean = DailyTaskBean().apply {
this.time = time
}
DatabaseWrapper.insert(bean)
taskBeans = DatabaseWrapper.loadAllTask()
dailyTaskAdapter.refresh(taskBeans)
dialog.dismiss()
}
dialog.show()
}
private fun importTask() {
AlertInputDialog.Builder()
.setContext(this)
.setTitle("导入任务")
.setHintMessage("请将导出的任务粘贴到这里")
.setNegativeButton("取消")
.setPositiveButton("确定")
.setOnDialogButtonClickListener(object :
AlertInputDialog.OnDialogButtonClickListener {
override fun onConfirmClick(value: String) {
when (val result = taskDataManager.importTasks(value)) {
is TaskDataManager.ImportResult.Success -> {
if (result.count > 0) {
taskBeans = DatabaseWrapper.loadAllTask()
dailyTaskAdapter.refresh(taskBeans)
binding.recyclerView.visibility = View.VISIBLE
binding.emptyView.visibility = View.GONE
}
"任务导入成功".show(context)
}
is TaskDataManager.ImportResult.Error -> {
result.message.show(context)
}
}
}
override fun onCancelClick() {}
}).build().show()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
LogFileManager.writeLog("onNewIntent: ${packageName}回到前台")
if (!maskViewController.isMaskVisible()) {
maskViewController.showMaskView(mainHandler)
}
}
override fun onDestroy() {
super.onDestroy()
maskViewController.destroy(mainHandler)
taskScheduler.destroy()
timeoutTimerManager.destroy()
mainHandler.removeCallbacksAndMessages(null)
EventBus.getDefault().unregister(this)
try {
unbindService(serviceConnection)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/ui/MessageChannelActivity.kt
================================================
package com.pengxh.daily.app.ui
import android.os.Bundle
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.pengxh.daily.app.databinding.ActivityMessageChannelBinding
import com.pengxh.daily.app.sqlite.DatabaseWrapper
import com.pengxh.daily.app.utils.Constant
import com.pengxh.daily.app.utils.EmailManager
import com.pengxh.daily.app.vm.MessageViewModel
import com.pengxh.kt.lite.base.KotlinBaseActivity
import com.pengxh.kt.lite.extensions.isEmail
import com.pengxh.kt.lite.extensions.show
import com.pengxh.kt.lite.utils.LoadingDialog
import com.pengxh.kt.lite.utils.SaveKeyValues
class MessageChannelActivity : KotlinBaseActivity() {
private val kTag = "MessageChannelActivity"
private val context = this
private val messageViewModel by lazy { ViewModelProvider(this)[MessageViewModel::class.java] }
private val emailManager by lazy { EmailManager(this) }
override fun initViewBinding(): ActivityMessageChannelBinding {
return ActivityMessageChannelBinding.inflate(layoutInflater)
}
override fun setupTopBarLayout() {
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { view, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
view.setPadding(0, statusBarHeight, 0, 0)
insets
}
binding.toolbar.setNavigationOnClickListener { finish() }
}
override fun initOnCreate(savedInstanceState: Bundle?) {
val title = SaveKeyValues.getValue(Constant.MESSAGE_TITLE_KEY, "打卡结果通知") as String
binding.messageTitleView.setText(title)
val type = SaveKeyValues.getValue(Constant.CHANNEL_TYPE_KEY, -1) as Int
if (type == 0) {
binding.wxRadioButton.isChecked = true
} else if (type == 1) {
binding.qqRadioButton.isChecked = true
}
val key = SaveKeyValues.getValue(Constant.WX_WEB_HOOK_KEY, "") as String
if (!key.isBlank()) {
binding.wxKeyView.setText(key)
}
val configs = DatabaseWrapper.loadAll()
if (configs.isNotEmpty()) {
configs.last().run {
val outbox = if (outbox.contains("@qq.com")) {
outbox.dropLast(7)
} else {
outbox
}
binding.emailSendAddressView.setText(outbox)
binding.emailSendCodeView.setText(authCode)
binding.emailInboxView.setText(inbox)
}
}
}
override fun observeRequestState() {
}
override fun initEvent() {
binding.wxRadioButton.setOnClickListener {
val key = SaveKeyValues.getValue(Constant.WX_WEB_HOOK_KEY, "") as String
if (binding.wxRadioButton.isChecked && key.isNotBlank()) {
SaveKeyValues.putValue(Constant.CHANNEL_TYPE_KEY, 0)
binding.qqRadioButton.isChecked = false
} else {
"请先配置企业微信消息 Webhook key".show(context)
binding.wxRadioButton.isChecked = false
}
}
binding.sendWxButton.setOnClickListener {
val key = binding.wxKeyView.text.toString()
if (key.isBlank()) {
"企业微信消息 Webhook key 为空".show(context)
return@setOnClickListener
}
SaveKeyValues.putValue(
Constant.MESSAGE_TITLE_KEY,
binding.messageTitleView.text.toString().trim()
)
SaveKeyValues.putValue(Constant.WX_WEB_HOOK_KEY, key)
MaterialAlertDialogBuilder(this)
.setTitle("测试消息")
.setMessage("企业微信配置完成,可以发送企业微信消息。\n\n是否继续?")
.setCancelable(false) // 禁止点击外部关闭
.setPositiveButton("继续") { _, _ ->
sendTestMessage()
}.setNegativeButton("取消", null).show()
}
binding.qqRadioButton.setOnClickListener {
val configs = DatabaseWrapper.loadAll()
if (binding.qqRadioButton.isChecked && configs.isNotEmpty()) {
SaveKeyValues.putValue(Constant.CHANNEL_TYPE_KEY, 1)
binding.wxRadioButton.isChecked = false
} else {
"请先配置QQ邮箱".show(context)
binding.qqRadioButton.isChecked = false
}
}
binding.sendEmailButton.setOnClickListener {
val address = binding.emailSendAddressView.text.toString()
val outbox = if (address.contains("@qq.com")) {
address
} else {
"${address}@qq.com"
}
if (outbox.isBlank()) {
"发件箱地址为空".show(context)
return@setOnClickListener
}
if (!outbox.isEmail()) {
"发件箱格式错误,请检查".show(context)
return@setOnClickListener
}
val authCode = binding.emailSendCodeView.text.toString()
if (authCode.isBlank()) {
"发件箱授权码为空".show(context)
return@setOnClickListener
}
val inbox = binding.emailInboxView.text.toString()
if (inbox.isBlank()) {
"收件箱地址为空".show(context)
return@setOnClickListener
}
if (!inbox.isEmail()) {
"发件箱格式错误,请检查".show(context)
return@setOnClickListener
}
SaveKeyValues.putValue(
Constant.MESSAGE_TITLE_KEY,
binding.messageTitleView.text.toString().trim()
)
DatabaseWrapper.insertConfig(outbox, authCode, inbox)
sendTestEmail()
}
}
private fun sendTestMessage() {
val message = buildString {
appendLine("你好!")
append("这是来自 DailyTask 的测试消息 🎉")
}
messageViewModel.sendMessage(
message,
onLoading = {
if (isFinishing || isDestroyed) return@sendMessage
LoadingDialog.show(this, "消息发送中,请稍后...")
},
onSuccess = {
if (isFinishing || isDestroyed) return@sendMessage
LoadingDialog.dismiss()
},
onFailed = {
if (isFinishing || isDestroyed) return@sendMessage
LoadingDialog.dismiss()
it.show(this)
})
}
private fun sendTestEmail() {
MaterialAlertDialogBuilder(this)
.setTitle("测试邮件")
.setMessage("QQ邮箱配置完成,可以发送QQ邮件。\n\n是否继续?")
.setCancelable(false) // 禁止点击外部关闭
.setPositiveButton("继续") { _, _ ->
LoadingDialog.show(context, "邮件发送中,请稍后....")
emailManager.sendEmail(
"邮箱测试", "这是一封测试邮件,不必关注",
true,
onSuccess = {
LoadingDialog.dismiss()
"发送成功,请注意查收".show(context)
},
onFailure = {
LoadingDialog.dismiss()
"发送失败:${it}".show(context)
}
)
}.setNegativeButton("取消", null).show()
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/ui/QuestionAndAnswerActivity.kt
================================================
package com.pengxh.daily.app.ui
import android.os.Build
import android.os.Bundle
import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.pengxh.daily.app.R
import com.pengxh.daily.app.databinding.ActivityQuestionAndAnswerBinding
import com.pengxh.daily.app.model.QuestionAnAnswerModel
import com.pengxh.kt.lite.adapter.NormalRecyclerAdapter
import com.pengxh.kt.lite.adapter.ViewHolder
import com.pengxh.kt.lite.base.KotlinBaseActivity
import com.pengxh.kt.lite.extensions.getStatusBarHeight
import com.pengxh.kt.lite.extensions.readAssetsFile
import com.pengxh.kt.lite.utils.HtmlRenderEngine
class QuestionAndAnswerActivity : KotlinBaseActivity() {
private val context = this
private val gson by lazy { Gson() }
override fun initEvent() {
}
override fun initOnCreate(savedInstanceState: Bundle?) {
binding.marqueeView.requestFocus()
val assetsFile = readAssetsFile("QuestionAndAnswer.json")
val dataRows = gson.fromJson>(
assetsFile, object : TypeToken>() {}.type
)
binding.recyclerView.adapter = object :
NormalRecyclerAdapter(R.layout.item_q_a_rv_l, dataRows) {
override fun convertView(
viewHolder: ViewHolder, position: Int, item: QuestionAnAnswerModel
) {
viewHolder.setText(R.id.questionView, "${position + 1}、${item.question}")
val textView = viewHolder.getView(R.id.answerView)
HtmlRenderEngine.Builder()
.setContext(context)
.setHtmlContent(item.answer)
.setTargetView(textView)
.setOnGetImageSourceListener(object :
HtmlRenderEngine.OnGetImageSourceListener {
override fun imageSource(url: String) {
}
}).build().load()
}
}
}
override fun initViewBinding(): ActivityQuestionAndAnswerBinding {
return ActivityQuestionAndAnswerBinding.inflate(layoutInflater)
}
override fun observeRequestState() {
}
override fun setupTopBarLayout() {
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { view, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
view.setPadding(0, statusBarHeight, 0, 0)
insets
}
binding.toolbar.setNavigationOnClickListener { finish() }
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/ui/SettingsActivity.kt
================================================
package com.pengxh.daily.app.ui
import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.pengxh.daily.app.BuildConfig
import com.pengxh.daily.app.R
import com.pengxh.daily.app.databinding.ActivitySettingsBinding
import com.pengxh.daily.app.extensions.notificationEnable
import com.pengxh.daily.app.extensions.openApplication
import com.pengxh.daily.app.service.CaptureImageService
import com.pengxh.daily.app.service.NotificationMonitorService
import com.pengxh.daily.app.utils.ApplicationEvent
import com.pengxh.daily.app.utils.Constant
import com.pengxh.daily.app.utils.DailyTask
import com.pengxh.daily.app.utils.EmailManager
import com.pengxh.daily.app.utils.ProjectionSession
import com.pengxh.daily.app.utils.WatermarkDrawable
import com.pengxh.daily.app.vm.MessageViewModel
import com.pengxh.kt.lite.base.KotlinBaseActivity
import com.pengxh.kt.lite.extensions.convertColor
import com.pengxh.kt.lite.extensions.navigatePageTo
import com.pengxh.kt.lite.extensions.show
import com.pengxh.kt.lite.utils.LoadingDialog
import com.pengxh.kt.lite.utils.SaveKeyValues
import com.pengxh.kt.lite.widget.dialog.BottomActionSheet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class SettingsActivity : KotlinBaseActivity() {
private val kTag = "SettingsActivity"
private val context = this
private val apps by lazy {
listOf(
"钉钉",
"企业微信",
"飞书",
"移动办公M3"
)
}
private val icons by lazy {
listOf(
R.drawable.ic_ding_ding,
R.drawable.ic_wei_xin,
R.drawable.ic_fei_shu,
R.mipmap.ic_mobile_m3
)
}
private val channels = arrayListOf("企业微信", "QQ邮箱")
private val notificationContract by lazy { ActivityResultContracts.StartActivityForResult() }
private val projectionContract by lazy { ActivityResultContracts.StartActivityForResult() }
private val mpr by lazy { getSystemService(MediaProjectionManager::class.java) }
private val messageViewModel by lazy { ViewModelProvider(this)[MessageViewModel::class.java] }
private val emailManager by lazy { EmailManager(this) }
override fun initViewBinding(): ActivitySettingsBinding {
return ActivitySettingsBinding.inflate(layoutInflater)
}
override fun setupTopBarLayout() {
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { view, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
view.setPadding(0, statusBarHeight, 0, 0)
insets
}
binding.toolbar.setNavigationOnClickListener { finish() }
}
override fun initOnCreate(savedInstanceState: Bundle?) {
EventBus.getDefault().register(this)
val index = SaveKeyValues.getValue(Constant.TARGET_APP_KEY, 0) as Int
binding.iconView.setBackgroundResource(icons[index])
binding.appVersion.text = BuildConfig.VERSION_NAME
if (notificationEnable()) {
turnOnNotificationMonitorService()
}
val watermark = DailyTask.getWatermarkText()
binding.contentView.background = WatermarkDrawable(this, watermark)
}
@Suppress("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
fun handleApplicationEvent(event: ApplicationEvent) {
when (event) {
is ApplicationEvent.ListenerConnected -> {
binding.noticeTipsView.text = "服务状态查询中,请稍后..."
binding.noticeTipsView.setTextColor(R.color.theme_color.convertColor(this))
binding.noticeSwitch.isChecked = true
binding.noticeTipsView.visibility = View.GONE
}
is ApplicationEvent.ListenerDisconnected -> {
binding.noticeTipsView.text = "服务未开启,无法监听打卡结果和接收远程指令"
binding.noticeTipsView.setTextColor(Color.RED)
binding.noticeSwitch.isChecked = false
binding.noticeTipsView.visibility = View.VISIBLE
}
is ApplicationEvent.ProjectionReady -> {
binding.captureSwitch.isChecked = true
binding.captureTipsView.visibility = View.GONE
}
is ApplicationEvent.ProjectionFailed -> {
"截屏服务启动失败,请重试".show(this)
binding.captureSwitch.isChecked = false
binding.captureTipsView.visibility = View.VISIBLE
}
is ApplicationEvent.CaptureCompleted -> {
val type = SaveKeyValues.getValue(Constant.CHANNEL_TYPE_KEY, -1) as Int
when (type) {
0 -> {
// 企业微信
messageViewModel.sendImageMessage(
event.imagePath, onLoading = {
if (isFinishing || isDestroyed) return@sendImageMessage
LoadingDialog.show(this, "消息发送中,请稍后...")
},
onSuccess = {
if (isFinishing || isDestroyed) return@sendImageMessage
LoadingDialog.dismiss()
},
onFailed = {
if (isFinishing || isDestroyed) return@sendImageMessage
LoadingDialog.dismiss()
it.show(this)
})
}
1 -> {
// QQ邮箱
LoadingDialog.show(this, "邮件发送中,请稍后....")
emailManager.sendAttachmentEmail(
"邮箱测试", "这是一封测试邮件,不必关注", event.imagePath, true,
onSuccess = {
LoadingDialog.dismiss()
"发送成功,请注意查收".show(this)
},
onFailure = {
LoadingDialog.dismiss()
"发送失败:${it}".show(this)
})
}
else -> "消息渠道不支持".show(this)
}
}
else -> {}
}
}
override fun observeRequestState() {
}
override fun initEvent() {
binding.targetAppLayout.setOnClickListener {
BottomActionSheet.Builder()
.setContext(this)
.setActionItemTitle(apps)
.setItemTextColor(R.color.theme_color.convertColor(this))
.setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener {
override fun onActionItemClick(position: Int) {
val oldPosition = SaveKeyValues.getValue(Constant.TARGET_APP_KEY, 0) as Int
// 如果 position 没有变化,直接返回
if (oldPosition == position) {
binding.iconView.setBackgroundResource(icons[position])
return
}
if (position == 1 || position == 2) {
// 企业微信或者飞书只能采用截屏获取打卡结果
if (binding.captureSwitch.isChecked) {
binding.captureRadioButton.isChecked = true
SaveKeyValues.putValue(Constant.RESULT_SOURCE_KEY, 1)
binding.noticeRadioButton.isChecked = false
} else {
"请先打开截屏服务".show(context)
binding.captureRadioButton.isChecked = false
return
}
}
// 更新配置
binding.iconView.setBackgroundResource(icons[position])
SaveKeyValues.putValue(Constant.TARGET_APP_KEY, position)
}
}).build().show()
}
binding.msgChannelLayout.setOnClickListener {
navigatePageTo()
}
binding.noticeRadioButton.setOnClickListener {
val index = SaveKeyValues.getValue(Constant.TARGET_APP_KEY, 0) as Int
if (index != 0) {
"通知监听仅支持钉钉打卡".show(this)
binding.noticeRadioButton.isChecked = false
return@setOnClickListener
}
if (binding.noticeSwitch.isChecked) {
binding.noticeRadioButton.isChecked = true
SaveKeyValues.putValue(Constant.RESULT_SOURCE_KEY, 0)
binding.captureRadioButton.isChecked = false
} else {
"请先打开通知监听".show(this)
binding.noticeRadioButton.isChecked = false
}
}
binding.captureRadioButton.setOnClickListener {
if (binding.captureSwitch.isChecked) {
binding.captureRadioButton.isChecked = true
SaveKeyValues.putValue(Constant.RESULT_SOURCE_KEY, 1)
binding.noticeRadioButton.isChecked = false
} else {
"请先打开截屏服务".show(this)
binding.captureRadioButton.isChecked = false
}
}
binding.taskConfigLayout.setOnClickListener {
navigatePageTo()
}
binding.noticeSwitch.setOnClickListener {
notificationSettingLauncher.launch(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
}
binding.captureSwitch.setOnClickListener {
projectionLauncher.launch(mpr.createScreenCaptureIntent())
}
binding.openTestLayout.setOnClickListener {
openApplication(false)
}
binding.captureTestLayout.setOnClickListener {
if (!binding.captureSwitch.isChecked) {
"请先打开截屏服务".show(this)
return@setOnClickListener
}
// 再次确认 session 实际状态
if (ProjectionSession.state != ProjectionSession.State.ACTIVE) {
binding.captureSwitch.isChecked = false
"截屏授权已失效,请重新授权".show(this)
return@setOnClickListener
}
EventBus.getDefault().post(ApplicationEvent.CaptureScreen)
}
binding.gestureDetectSwitch.setOnCheckedChangeListener { _, isChecked ->
SaveKeyValues.putValue(Constant.GESTURE_DETECTOR_KEY, isChecked)
}
binding.backToHomeSwitch.setOnCheckedChangeListener { _, isChecked ->
SaveKeyValues.putValue(Constant.BACK_TO_HOME_KEY, isChecked)
}
binding.introduceLayout.setOnClickListener {
navigatePageTo()
}
}
private val projectionLauncher = registerForActivityResult(projectionContract) {
if (it.resultCode != RESULT_OK) {
"用户拒绝授权".show(this)
return@registerForActivityResult
}
val data = it.data ?: run {
"授权失败".show(this)
return@registerForActivityResult
}
if (ProjectionSession.state == ProjectionSession.State.ACTIVE) {
Log.d(kTag, "MediaProjection already active, skipping creation")
return@registerForActivityResult
}
Intent(this, CaptureImageService::class.java).apply {
putExtra("resultCode", it.resultCode)
putExtra("data", data)
startForegroundService(this)
}
}
private val notificationSettingLauncher = registerForActivityResult(notificationContract) {
if (notificationEnable()) {
turnOnNotificationMonitorService()
}
}
override fun onResume() {
super.onResume()
val type = SaveKeyValues.getValue(Constant.CHANNEL_TYPE_KEY, -1) as Int
if (type in 0..channels.lastIndex) {
binding.channelView.text = channels[type]
binding.channelView.setTextColor(R.color.theme_color.convertColor(this))
} else {
binding.channelView.text = "未配置"
binding.channelView.setTextColor(R.color.red.convertColor(this))
}
val resultSource = SaveKeyValues.getValue(Constant.RESULT_SOURCE_KEY, -1) as Int
if (resultSource == 0) {
binding.noticeRadioButton.isChecked = true
binding.captureRadioButton.isChecked = false
} else {
if (ProjectionSession.state == ProjectionSession.State.ACTIVE) {
binding.captureRadioButton.isChecked = true
binding.noticeRadioButton.isChecked = false
} else {
binding.captureRadioButton.isChecked = false
binding.noticeRadioButton.isChecked = true
}
}
binding.gestureDetectSwitch.isChecked =
SaveKeyValues.getValue(Constant.GESTURE_DETECTOR_KEY, true) as Boolean
binding.backToHomeSwitch.isChecked =
SaveKeyValues.getValue(Constant.BACK_TO_HOME_KEY, true) as Boolean
if (notificationEnable()) {
binding.noticeTipsView.text = "服务状态查询中,请稍后..."
binding.noticeTipsView.setTextColor(R.color.theme_color.convertColor(this))
lifecycleScope.launch(Dispatchers.Main) {
delay(500)
if (notificationEnable()) {
binding.noticeSwitch.isChecked = true
binding.noticeTipsView.visibility = View.GONE
}
}
} else {
binding.noticeTipsView.text = "服务未开启,无法监听打卡结果和接收远程指令"
binding.noticeTipsView.setTextColor(Color.RED)
binding.noticeSwitch.isChecked = false
binding.noticeTipsView.visibility = View.VISIBLE
}
if (ProjectionSession.state == ProjectionSession.State.ACTIVE) {
binding.captureSwitch.isChecked = true
binding.captureTipsView.visibility = View.GONE
} else {
binding.captureTipsView.text = "截屏服务未开启,无法获取打卡结果"
binding.captureTipsView.setTextColor(Color.RED)
binding.captureSwitch.isChecked = false
binding.captureTipsView.visibility = View.VISIBLE
}
}
private fun turnOnNotificationMonitorService() {
lifecycleScope.launch(Dispatchers.IO) {
try {
val componentName = ComponentName(context, NotificationMonitorService::class.java)
// 检查当前组件状态
val currentState = context.packageManager.getComponentEnabledSetting(componentName)
if (currentState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
// 如果已经启用,先禁用
context.packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
delay(500) // 短暂延迟
}
// 重新启用
context.packageManager.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun onDestroy() {
super.onDestroy()
EventBus.getDefault().unregister(this)
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/ui/TaskConfigActivity.kt
================================================
package com.pengxh.daily.app.ui
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.pengxh.daily.app.R
import com.pengxh.daily.app.databinding.ActivityTaskConfigBinding
import com.pengxh.daily.app.extensions.isApplicationExist
import com.pengxh.daily.app.model.ExportDataModel
import com.pengxh.daily.app.sqlite.DatabaseWrapper
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean
import com.pengxh.daily.app.sqlite.bean.EmailConfigBean
import com.pengxh.daily.app.utils.AlarmScheduler
import com.pengxh.daily.app.utils.ApplicationEvent
import com.pengxh.daily.app.utils.Constant
import com.pengxh.kt.lite.base.KotlinBaseActivity
import com.pengxh.kt.lite.extensions.convertColor
import com.pengxh.kt.lite.extensions.isNumber
import com.pengxh.kt.lite.extensions.show
import com.pengxh.kt.lite.extensions.toJson
import com.pengxh.kt.lite.utils.SaveKeyValues
import com.pengxh.kt.lite.widget.dialog.AlertInputDialog
import com.pengxh.kt.lite.widget.dialog.BottomActionSheet
import org.greenrobot.eventbus.EventBus
class TaskConfigActivity : KotlinBaseActivity() {
private val kTag = "TaskConfigActivity"
private val context = this
private val hourArray = arrayListOf("0", "1", "2", "3", "4", "5", "6", "自定义(单位:时)")
private val timeArray = arrayListOf("15", "30", "45", "自定义(单位:秒)")
private val optionArray = arrayListOf("QQ", "微信", "TIM", "支付宝", "剪切板")
private val clipboard by lazy { getSystemService(ClipboardManager::class.java) }
override fun initViewBinding(): ActivityTaskConfigBinding {
return ActivityTaskConfigBinding.inflate(layoutInflater)
}
override fun observeRequestState() {
}
override fun setupTopBarLayout() {
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { view, insets ->
val statusBarHeight = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top
view.setPadding(0, statusBarHeight, 0, 0)
insets
}
binding.toolbar.setNavigationOnClickListener { finish() }
}
override fun initOnCreate(savedInstanceState: Bundle?) {
val hour = SaveKeyValues.getValue(
Constant.RESET_TIME_KEY, Constant.DEFAULT_RESET_HOUR
) as Int
binding.resetTimeView.text = "每天${hour}点"
val time = SaveKeyValues.getValue(
Constant.STAY_DD_TIMEOUT_KEY, Constant.DEFAULT_OVER_TIME
) as Int
binding.timeoutTextView.text = "${time}s"
binding.keyTextView.text =
SaveKeyValues.getValue(Constant.TASK_COMMAND_KEY, "打卡") as String
binding.autoTaskSwitch.isChecked = SaveKeyValues.getValue(
Constant.TASK_AUTO_START_KEY, true
) as Boolean
val needRandom = SaveKeyValues.getValue(Constant.RANDOM_TIME_KEY, true) as Boolean
binding.randomTimeSwitch.isChecked = needRandom
if (needRandom) {
binding.minuteRangeLayout.visibility = View.VISIBLE
val value = SaveKeyValues.getValue(Constant.RANDOM_MINUTE_RANGE_KEY, 5) as Int
binding.minuteRangeView.text = "${value}分钟"
} else {
binding.minuteRangeLayout.visibility = View.GONE
}
}
override fun initEvent() {
binding.resetTimeLayout.setOnClickListener {
BottomActionSheet.Builder()
.setContext(this)
.setActionItemTitle(hourArray)
.setItemTextColor(R.color.theme_color.convertColor(this))
.setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener {
override fun onActionItemClick(position: Int) {
setHourByPosition(position)
}
}).build().show()
}
binding.timeoutLayout.setOnClickListener {
BottomActionSheet.Builder()
.setContext(this)
.setActionItemTitle(timeArray)
.setItemTextColor(R.color.theme_color.convertColor(this))
.setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener {
override fun onActionItemClick(position: Int) {
setTimeByPosition(position)
}
}).build().show()
}
binding.keyLayout.setOnClickListener {
AlertInputDialog.Builder()
.setContext(this)
.setTitle("设置打卡口令")
.setHintMessage("请输入打卡口令,如:打卡")
.setNegativeButton("取消")
.setPositiveButton("确定")
.setOnDialogButtonClickListener(object :
AlertInputDialog.OnDialogButtonClickListener {
override fun onConfirmClick(value: String) {
SaveKeyValues.putValue(Constant.TASK_COMMAND_KEY, value)
binding.keyTextView.text = value
}
override fun onCancelClick() {}
}).build().show()
}
binding.randomTimeSwitch.setOnCheckedChangeListener { _, isChecked ->
SaveKeyValues.putValue(Constant.RANDOM_TIME_KEY, isChecked)
if (isChecked) {
binding.minuteRangeLayout.visibility = View.VISIBLE
val value = SaveKeyValues.getValue(Constant.RANDOM_MINUTE_RANGE_KEY, 5) as Int
binding.minuteRangeView.text = "${value}分钟"
} else {
binding.minuteRangeLayout.visibility = View.GONE
}
}
binding.minuteRangeLayout.setOnClickListener {
AlertInputDialog.Builder()
.setContext(this)
.setTitle("设置随机时间范围")
.setHintMessage("请输入整数,如:30")
.setNegativeButton("取消")
.setPositiveButton("确定")
.setOnDialogButtonClickListener(object :
AlertInputDialog.OnDialogButtonClickListener {
override fun onConfirmClick(value: String) {
if (value.isNumber()) {
binding.minuteRangeView.text = "${value}分钟"
SaveKeyValues.putValue(Constant.RANDOM_MINUTE_RANGE_KEY, value.toInt())
} else {
"直接输入整数时间即可".show(context)
}
}
override fun onCancelClick() {}
}).build().show()
}
binding.outputLayout.setOnClickListener {
val exportData = ExportDataModel()
val taskBeans = DatabaseWrapper.loadAllTask()
if (taskBeans.isNotEmpty()) {
exportData.tasks = taskBeans
} else {
exportData.tasks = ArrayList()
}
val title = SaveKeyValues.getValue(Constant.MESSAGE_TITLE_KEY, "打卡结果通知") as String
exportData.messageTitle = title
val key = SaveKeyValues.getValue(Constant.WX_WEB_HOOK_KEY, "") as String
exportData.wxKey = key
val configs = DatabaseWrapper.loadAll()
if (configs.isNotEmpty()) {
exportData.emailConfig = configs.last()
} else {
exportData.emailConfig = EmailConfigBean()
}
val isDetectGesture = SaveKeyValues.getValue(
Constant.GESTURE_DETECTOR_KEY, false
) as Boolean
exportData.isDetectGesture = isDetectGesture
val isBackToHome = SaveKeyValues.getValue(
Constant.BACK_TO_HOME_KEY, false
) as Boolean
exportData.isBackToHome = isBackToHome
val hour = SaveKeyValues.getValue(
Constant.RESET_TIME_KEY, Constant.DEFAULT_RESET_HOUR
) as Int
exportData.resetTime = hour
val time = SaveKeyValues.getValue(
Constant.STAY_DD_TIMEOUT_KEY, Constant.DEFAULT_OVER_TIME
) as Int
exportData.overTime = time
val command = SaveKeyValues.getValue(Constant.TASK_COMMAND_KEY, "打卡") as String
exportData.command = command
exportData.isAutoStart = SaveKeyValues.getValue(
Constant.TASK_AUTO_START_KEY, true
) as Boolean
exportData.isRandomTime = SaveKeyValues.getValue(
Constant.RANDOM_TIME_KEY, true
) as Boolean
val value = SaveKeyValues.getValue(Constant.RANDOM_MINUTE_RANGE_KEY, 5) as Int
exportData.timeRange = value
val json = exportData.toJson()
Log.d(kTag, json)
// 分享
BottomActionSheet.Builder()
.setContext(this)
.setActionItemTitle(optionArray)
.setItemTextColor(R.color.theme_color.convertColor(this))
.setOnActionSheetListener(object : BottomActionSheet.OnActionSheetListener {
override fun onActionItemClick(position: Int) {
when (position) {
0 -> shareTextTo(Constant.QQ, "QQ", json)
1 -> shareTextTo(Constant.WECHAT, "微信", json)
2 -> shareTextTo(Constant.TIM, "TIM", json)
3 -> shareTextTo(Constant.ZFB, "支付宝", json)
4 -> {
val cipData = ClipData.newPlainText("TaskConfig", json)
clipboard.setPrimaryClip(cipData)
"已复制到剪切板".show(context)
}
}
}
}).build().show()
}
}
private fun setHourByPosition(position: Int) {
if (position == hourArray.size - 1) {
AlertInputDialog.Builder()
.setContext(this)
.setTitle("设置重置时间")
.setHintMessage("直接输入整数时间即可,如:6")
.setNegativeButton("取消")
.setPositiveButton("确定")
.setOnDialogButtonClickListener(object :
AlertInputDialog.OnDialogButtonClickListener {
override fun onConfirmClick(value: String) {
if (value.isNumber()) {
val hour = value.toInt()
binding.resetTimeView.text = "每天${hour}点"
setTaskResetTime(hour)
} else {
"直接输入整数时间即可".show(context)
}
}
override fun onCancelClick() {}
}).build().show()
} else {
val hour = hourArray[position].toInt()
binding.resetTimeView.text = "每天${hour}点"
setTaskResetTime(hour)
}
}
private fun setTaskResetTime(hour: Int) {
SaveKeyValues.putValue(Constant.RESET_TIME_KEY, hour)
// 取消旧 Alarm,注册新时间点的 Alarm
AlarmScheduler.cancel(this)
AlarmScheduler.schedule(this, hour)
// 通知 Service 更新倒计时显示
EventBus.getDefault().post(ApplicationEvent.SetResetTaskTime)
}
private fun setTimeByPosition(position: Int) {
if (position == timeArray.size - 1) {
AlertInputDialog.Builder()
.setContext(this)
.setTitle("设置超时时间")
.setHintMessage("直接输入整数时间即可,如:60")
.setNegativeButton("取消")
.setPositiveButton("确定")
.setOnDialogButtonClickListener(object :
AlertInputDialog.OnDialogButtonClickListener {
override fun onConfirmClick(value: String) {
if (value.isNumber()) {
val time = value.toInt()
binding.timeoutTextView.text = "${time}s"
updateDingDingTimeout(time)
} else {
"直接输入整数时间即可".show(context)
}
}
override fun onCancelClick() {}
}).build().show()
} else {
val time = timeArray[position].toInt()
binding.timeoutTextView.text = "${time}s"
updateDingDingTimeout(time)
}
}
private fun shareTextTo(packageName: String, appName: String, text: String) {
if (!isApplicationExist(packageName)) {
"请先安装${appName}".show(this)
return
}
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, text)
setPackage(packageName)
}
try {
startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
"分享失败".show(this)
}
}
private fun updateDingDingTimeout(time: Int) {
SaveKeyValues.putValue(Constant.STAY_DD_TIMEOUT_KEY, time)
// 更新目标应用任务超时时间
EventBus.getDefault().post(ApplicationEvent.SetTaskOvertime(time))
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/AlarmScheduler.kt
================================================
package com.pengxh.daily.app.utils
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import java.util.Calendar
object AlarmScheduler {
/**
* 注册下一次重置 Alarm(精确到整点)
*/
fun schedule(context: Context, hour: Int) {
val alarmManager = context.getSystemService(AlarmManager::class.java)
val pendingIntent = buildPendingIntent(context)
// 计算下一次触发时间
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
// 如果今天的时间点已过,则设为明天
if (timeInMillis <= System.currentTimeMillis()) {
add(Calendar.DATE, 1)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
}
} else {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
}
}
/**
* 取消已注册的 Alarm
*/
fun cancel(context: Context) {
val alarmManager = context.getSystemService(AlarmManager::class.java)
alarmManager.cancel(buildPendingIntent(context))
}
private fun buildPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, TaskResetReceiver::class.java)
return PendingIntent.getBroadcast(
context,
10001,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/ApplicationEvent.kt
================================================
package com.pengxh.daily.app.utils
/**
* 应用内事件定义
* 统一使用EventBus进行应用内组件通信
*/
sealed class ApplicationEvent {
/**
* 蒙版视图控制事件
*/
object ShowMaskView : ApplicationEvent()
object HideMaskView : ApplicationEvent()
/**
* 监听器状态事件
*/
object ListenerConnected : ApplicationEvent()
object ListenerDisconnected : ApplicationEvent()
/**
* 任务控制事件
*/
object StartDailyTask : ApplicationEvent()
object StopDailyTask : ApplicationEvent()
object SetResetTaskTime : ApplicationEvent()
data class UpdateResetTickTime(val countDownTime: String) : ApplicationEvent()
object ResetDailyTask : ApplicationEvent()
/**
* 悬浮窗控制事件
*/
object ShowFloatingWindow : ApplicationEvent()
object HideFloatingWindow : ApplicationEvent()
data class StartCountdownTime(val isRemoteCommand: Boolean) : ApplicationEvent()
data class UpdateFloatingViewTime(val tick: Int) : ApplicationEvent()
data class SetTaskOvertime(val time: Int) : ApplicationEvent()
/**
* 导航事件
*/
object GoBackMainActivity : ApplicationEvent()
/**
* 截屏事件
*/
object CaptureScreen : ApplicationEvent()
data class CaptureCompleted(val imagePath: String) : ApplicationEvent()
/**
* 投影截屏事件
*/
object ProjectionReady : ApplicationEvent()
object ProjectionFailed : ApplicationEvent()
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/Constant.kt
================================================
package com.pengxh.daily.app.utils
import com.pengxh.kt.lite.utils.SaveKeyValues
/**
* @author: Pengxh
* @email: 290677893@qq.com
* @date: 2019/12/29 12:42
*/
object Constant {
// ============================================================
// SharedPreferences 键
// ============================================================
const val RESET_TIME_KEY = "RESET_TIME_KEY"
const val STAY_DD_TIMEOUT_KEY = "STAY_DD_TIMEOUT_KEY"
const val GESTURE_DETECTOR_KEY = "GESTURE_DETECTOR_KEY"
const val BACK_TO_HOME_KEY = "BACK_TO_HOME_KEY"
const val TASK_COMMAND_KEY = "TASK_COMMAND_KEY"
const val RANDOM_TIME_KEY = "RANDOM_TIME_KEY"
const val RANDOM_MINUTE_RANGE_KEY = "RANDOM_MINUTE_RANGE_KEY"
const val TASK_AUTO_START_KEY = "TASK_AUTO_START_KEY"
const val TARGET_APP_KEY = "TARGET_APP_KEY"
const val MESSAGE_TITLE_KEY = "MESSAGE_TITLE_KEY"
const val WX_WEB_HOOK_KEY = "WX_WEB_HOOK_KEY"
const val CHANNEL_TYPE_KEY = "CHANNEL_TYPE_KEY"
const val RESULT_SOURCE_KEY = "RESULT_SOURCE_KEY"
// ============================================================
// 目标应用
// ============================================================
const val DING_DING = "com.alibaba.android.rimet" // 钉钉
const val WEWORK = "com.tencent.wework" // 企业微信
const val FEI_SHU = "com.ss.android.lark" // 飞书
const val MOBILE_M3 = "com.seeyon.cmp" // 移动办公M3
// ============================================================
// 消息指令
// ============================================================
const val WECHAT = "com.tencent.mm" // 微信
const val QQ = "com.tencent.mobileqq" // QQ
const val TIM = "com.tencent.tim" // TIM
const val ZFB = "com.eg.android.AlipayGphone" // 支付宝
// ============================================================
// webhook
// ============================================================
const val WX_WEB_HOOK_URL = "https://qyapi.weixin.qq.com"
// ============================================================
// 其他默认值
// ============================================================
const val DEFAULT_RESET_HOUR = 0
const val DEFAULT_OVER_TIME = 30
const val CAPTURE_IMAGE_SERVICE_NOTIFICATION_ID = 1001
const val COUNTDOWN_TIMER_SERVICE_NOTIFICATION_ID = 1002
const val FOREGROUND_RUNNING_SERVICE_NOTIFICATION_ID = 1003
// 目标APP
fun getTargetApp(): String {
return when (SaveKeyValues.getValue(TARGET_APP_KEY, 0) as Int) {
0 -> DING_DING
1 -> WEWORK
2 -> FEI_SHU
3 -> MOBILE_M3
else -> DING_DING
}
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/DailyTask.kt
================================================
package com.pengxh.daily.app.utils
object DailyTask {
init {
System.loadLibrary("daily_task")
}
external fun getWatermarkText(): String
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/EmailAuthenticator.kt
================================================
package com.pengxh.daily.app.utils
import javax.mail.Authenticator
import javax.mail.PasswordAuthentication
/**
* @author: Pengxh
* @email: 290677893@qq.com
* @date: 2020/1/16 15:42
*/
class EmailAuthenticator(private val username: String, private val authCode: String) :
Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication(username, authCode)
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/EmailManager.kt
================================================
package com.pengxh.daily.app.utils
import android.content.Context
import android.os.BatteryManager
import android.util.Log
import com.pengxh.daily.app.BuildConfig
import com.pengxh.daily.app.sqlite.DatabaseWrapper
import com.pengxh.kt.lite.extensions.timestampToDate
import com.pengxh.kt.lite.extensions.toJson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Date
import java.util.Properties
import javax.activation.DataHandler
import javax.activation.FileDataSource
import javax.mail.Message
import javax.mail.Session
import javax.mail.Transport
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeBodyPart
import javax.mail.internet.MimeMessage
import javax.mail.internet.MimeMultipart
class EmailManager(private val context: Context) {
private val kTag = "EmailManager"
private val batteryManager by lazy { context.getSystemService(BatteryManager::class.java) }
private fun createSmtpProperties(): Properties {
val props = Properties().apply {
put("mail.smtp.host", "smtp.qq.com")
put("mail.smtp.port", "465")
put("mail.smtp.auth", "true")
put("mail.smtp.ssl.checkserveridentity", "true")
put("mail.smtp.ssl.enable", "true")
put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory")
put("mail.smtp.socketFactory.port", "465")
}
return props
}
/**
* 发送普通邮件
*/
fun sendEmail(
title: String,
content: String,
isTest: Boolean,
onSuccess: (() -> Unit)? = null,
onFailure: ((String) -> Unit)? = null
) {
val configs = DatabaseWrapper.loadAll()
if (configs.isEmpty()) {
onFailure?.invoke("邮箱未配置,无法发送邮件")
return
}
val config = configs.last()
Log.d(kTag, "邮箱配置: ${config.toJson()}")
val authenticator = EmailAuthenticator(config.outbox, config.authCode)
val props = createSmtpProperties()
val session = Session.getInstance(props, authenticator)
val battery =
batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
val content = buildString {
appendLine(content)
appendLine("当前日期:${System.currentTimeMillis().timestampToDate()}")
appendLine("当前电量:${if (battery >= 0) "$battery%" else "未知"}")
append("版本号:${BuildConfig.VERSION_NAME}")
}
val message = MimeMessage(session).apply {
setFrom(InternetAddress(config.outbox))
setRecipient(Message.RecipientType.TO, InternetAddress(config.inbox))
subject = title
sentDate = Date()
setText(content)
}
sendAsync(message, isTest, onSuccess, onFailure)
}
/**
* 发送带附件的邮件
*/
fun sendAttachmentEmail(
title: String,
content: String,
filePath: String,
isTest: Boolean,
onSuccess: (() -> Unit)? = null,
onFailure: ((String) -> Unit)? = null
) {
val configs = DatabaseWrapper.loadAll()
if (configs.isEmpty()) {
onFailure?.invoke("邮箱未配置,无法发送邮件")
return
}
val config = configs.last()
Log.d(kTag, "邮箱配置: ${config.toJson()}")
val authenticator = EmailAuthenticator(config.outbox, config.authCode)
val props = createSmtpProperties()
val session = Session.getInstance(props, authenticator)
// 正文部分
val textPart = MimeBodyPart().apply {
setText(content)
}
// 附件部分
val attachmentPart = MimeBodyPart().apply {
val file = File(filePath)
dataHandler = DataHandler(FileDataSource(file))
fileName = file.name
}
// 组合 multipart
val multipart = MimeMultipart().apply {
addBodyPart(textPart)
addBodyPart(attachmentPart)
}
val message = MimeMessage(session).apply {
setFrom(InternetAddress(config.outbox))
setRecipient(Message.RecipientType.TO, InternetAddress(config.inbox))
subject = title
sentDate = Date()
setContent(multipart)
}
sendAsync(message, isTest, onSuccess, onFailure)
}
/**
* 异步发送邮件
*/
private fun sendAsync(
message: MimeMessage,
isTest: Boolean,
onSuccess: (() -> Unit)? = null,
onFailure: ((String) -> Unit)? = null
) {
CoroutineScope(Dispatchers.IO).launch {
try {
Transport.send(message)
if (isTest) {
withContext(Dispatchers.Main) {
onSuccess?.invoke()
}
}
} catch (e: Exception) {
if (isTest) {
val errorMessage = when {
e.message?.contains("535", ignoreCase = true) == true ->
"邮箱认证失败,请检查邮箱账号和授权码是否正确"
e.message?.contains("authentication failed", ignoreCase = true) == true ->
"邮箱认证失败,请确认使用的是授权码而非登录密码"
else -> "邮件发送失败: ${e.javaClass.simpleName} - ${e.message}"
}
withContext(Dispatchers.Main) {
onFailure?.invoke(errorMessage)
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/GestureController.kt
================================================
package com.pengxh.daily.app.utils
import android.content.Context
import android.os.Handler
import android.view.GestureDetector
import android.view.MotionEvent
import com.pengxh.kt.lite.utils.SaveKeyValues
/**
* 手势控制器
*
* 职责:
* 1. 管理滑动手势检测
* 2. 根据手势操作控制蒙层显示/隐藏
* 3. 提供手势开关配置
*
* @param context 上下文
* @param maskViewController 蒙层视图控制器
* @param mainHandler 主线程Handler
*/
class GestureController(
private val context: Context,
private val maskViewController: MaskViewController,
private val mainHandler: Handler
) {
private val minFlingDistance = 1000f
private val gestureDetector: GestureDetector
init {
gestureDetector = GestureDetector(context, GestureListener())
}
/**
* 处理触摸事件
*
* @param event 触摸事件
*/
fun onTouchEvent(event: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(event)
}
/**
* 手势监听器
*/
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onFling(
e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float
): Boolean {
val isGestureEnabled =
SaveKeyValues.getValue(Constant.GESTURE_DETECTOR_KEY, false) as Boolean
// 如果手势未启用,则不处理
if (!isGestureEnabled) {
return false
}
// 计算垂直滑动距离
val deltaY = calculateDeltaY(e1, e2)
// 处理从上向下滑动手势(显示蒙层)
if (isSwipeDown(deltaY, e1, e2)) {
handleShowMask()
return true
}
// 处理从下向上滑动手势(隐藏蒙层)
if (isSwipeUp(deltaY, e1, e2)) {
handleHideMask()
return true
}
return super.onFling(e1, e2, velocityX, velocityY)
}
}
/**
* 计算垂直滑动距离
*/
private fun calculateDeltaY(e1: MotionEvent?, e2: MotionEvent): Float {
return kotlin.math.abs(e2.y - (e1?.y ?: e2.y))
}
/**
* 判断是否为向下滑动手势
*/
private fun isSwipeDown(deltaY: Float, e1: MotionEvent?, e2: MotionEvent): Boolean {
return deltaY > minFlingDistance
&& (e2.y - (e1?.y ?: e2.y)) > 0
&& !maskViewController.isMaskVisible()
}
/**
* 判断是否为向上滑动手势
*/
private fun isSwipeUp(deltaY: Float, e1: MotionEvent?, e2: MotionEvent): Boolean {
return deltaY > minFlingDistance
&& (e2.y - (e1?.y ?: e2.y)) < 0
&& maskViewController.isMaskVisible()
}
/**
* 处理显示蒙层
*/
private fun handleShowMask() {
if (!maskViewController.isMaskVisible()) {
maskViewController.showMaskView(mainHandler)
}
}
/**
* 处理隐藏蒙层
*/
private fun handleHideMask() {
if (maskViewController.isMaskVisible()) {
maskViewController.hideMaskView(mainHandler)
}
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/HttpRequestManager.kt
================================================
package com.pengxh.daily.app.utils
import android.content.Context
import android.os.BatteryManager
import android.util.Log
import com.pengxh.daily.app.BuildConfig
import com.pengxh.kt.lite.extensions.timestampToDate
import com.pengxh.kt.lite.utils.SaveKeyValues
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONObject
import java.util.concurrent.TimeUnit
class HttpRequestManager(private val context: Context) {
private val kTag = "HttpRequestManager"
private val okHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build()
}
private val batteryManager by lazy { context.getSystemService(BatteryManager::class.java) }
fun sendMessage(title: String, message: String) {
CoroutineScope(Dispatchers.IO).launch {
val webhookKey = SaveKeyValues.getValue(Constant.WX_WEB_HOOK_KEY, "") as String
if (webhookKey.isBlank()) {
Log.e(kTag, "企业微信 Webhook Key 未配置")
return@launch
}
val url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=$webhookKey"
val battery = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
val content = buildString {
appendLine(title)
appendLine(message)
appendLine("当前日期:${System.currentTimeMillis().timestampToDate()}")
appendLine("当前电量:${if (battery >= 0) "$battery%" else "未知"}")
append("版本号:${BuildConfig.VERSION_NAME}")
}
val jsonBody = JSONObject().apply {
put("msgtype", "text")
put("text", JSONObject().apply {
put("content", content)
})
}
val requestBody = jsonBody.toString()
.toRequestBody("application/json; charset=utf-8".toMediaType())
val request = Request.Builder().url(url).post(requestBody).build()
try {
val response = okHttpClient.newCall(request).execute()
val responseBody = response.body.string()
Log.d(kTag, "响应: $responseBody")
} catch (e: Exception) {
Log.e(kTag, "发送失败", e)
}
}
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/LogFileManager.kt
================================================
package com.pengxh.daily.app.utils
import android.content.Context
import android.os.Environment
import android.util.Log
import com.pengxh.kt.lite.extensions.timestampToCompleteDate
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import java.util.concurrent.locks.ReentrantLock
import java.util.stream.Collectors
object LogFileManager {
private val kTag = "LogFileManager"
private const val MAX_LOG_SIZE = 5 * 1024 * 1024 // 5MB
private const val MAX_LOG_FILES = 5 // 最多保留5个日志文件
private lateinit var currentLogFile: Path
private val fileLock = ReentrantLock() // 防止并发写入冲突
@Synchronized
fun initLogFile(context: Context) {
val documentDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
?: throw IllegalStateException("External storage directory not available")
val logDir = documentDir.toPath()
currentLogFile = logDir.resolve("app_runtime_log.txt")
try {
if (!Files.exists(currentLogFile)) {
Files.createFile(currentLogFile)
} else if (Files.size(currentLogFile) > MAX_LOG_SIZE) {
rotateLogFiles(logDir)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
@Synchronized
private fun rotateLogFiles(directory: Path) {
fileLock.lock()
try {
if (!Files.exists(directory)) {
Files.createDirectories(directory)
}
// 获取并按时间戳排序日志文件
val logFiles = Files.list(directory).use { stream ->
stream.filter { path ->
val name = path.fileName.toString()
name.startsWith("app_runtime_log_") && name.endsWith(".txt")
}.map { path ->
val name = path.fileName.toString()
val timestampStr = name.removePrefix("app_runtime_log_").removeSuffix(".txt")
timestampStr.toLongOrNull()?.let { timestamp -> path to timestamp }
}.filter { it != null }.map { it }.collect(Collectors.toList())
}.sortedBy { it.second }.map { it.first }
// 如果日志数量达到上限,删除最早的
if (logFiles.size >= MAX_LOG_FILES) {
Files.deleteIfExists(logFiles.first())
}
// 生成新日志文件名
val newTimestamp = System.currentTimeMillis()
val newLogFile = directory.resolve("app_runtime_log_$newTimestamp.txt")
// 重命名当前日志文件
Files.move(currentLogFile, newLogFile)
// 创建新的空日志文件
Files.createFile(currentLogFile)
} catch (e: IOException) {
e.printStackTrace()
} finally {
fileLock.unlock()
}
}
@Synchronized
fun writeLog(log: String) {
if (::currentLogFile.isInitialized) {
fileLock.lock()
try {
Log.d(kTag, log)
val time = System.currentTimeMillis().timestampToCompleteDate()
val str = "$time ${log}${System.lineSeparator()}"
Files.write(currentLogFile, str.toByteArray(), StandardOpenOption.APPEND)
} catch (e: IOException) {
e.printStackTrace()
} finally {
fileLock.unlock()
}
} else {
throw IllegalStateException("Log file not initialized. Call initLogFile first.")
}
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/MaskViewController.kt
================================================
package com.pengxh.daily.app.utils
import android.os.Handler
import android.view.View
import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.Animation
import android.view.animation.ScaleAnimation
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import com.pengxh.daily.app.databinding.ActivityMainBinding
import com.pengxh.kt.lite.extensions.setScreenBrightness
import org.greenrobot.eventbus.EventBus
import java.util.Random
class MaskViewController(
private val activity: AppCompatActivity,
private val binding: ActivityMainBinding,
private val insetsController: WindowInsetsControllerCompat
) {
private var currentAnimation: Animation? = null
private val random = Random()
private var clockAnimationRunnable: Runnable? = null
fun showMaskView(handler: Handler) {
// 隐藏悬浮窗
EventBus.getDefault().post(ApplicationEvent.HideFloatingWindow)
// 隐藏系统栏
insetsController.apply {
hide(WindowInsetsCompat.Type.statusBars())
hide(WindowInsetsCompat.Type.navigationBars())
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
// 显示蒙层
binding.maskView.visibility = View.VISIBLE
currentAnimation?.cancel()
currentAnimation = ScaleAnimation(1.0f, 1.0f, 0.0f, 1.0f).apply {
duration = 500
}
binding.maskView.startAnimation(currentAnimation)
// 关闭屏幕亮度
activity.window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_OFF)
// 隐藏任务界面
binding.rootView.visibility = View.GONE
// 启动时钟动画
startClockAnimation(handler)
}
fun hideMaskView(handler: Handler) {
// 显示悬浮窗
EventBus.getDefault().post(ApplicationEvent.ShowFloatingWindow)
// 停止时钟动画
stopClockAnimation(handler)
// 恢复系统栏
insetsController.apply {
show(WindowInsetsCompat.Type.statusBars())
show(WindowInsetsCompat.Type.navigationBars())
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
}
// 隐藏蒙层
currentAnimation?.cancel()
currentAnimation = ScaleAnimation(1.0f, 1.0f, 1.0f, 0.0f).apply {
duration = 500
}
binding.maskView.startAnimation(currentAnimation)
// 恢复屏幕亮度
activity.window.setScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE)
binding.maskView.visibility = View.GONE
binding.rootView.visibility = View.VISIBLE
}
fun isMaskVisible(): Boolean = binding.maskView.isVisible
private fun startClockAnimation(handler: Handler) {
clockAnimationRunnable = object : Runnable {
override fun run() {
if (binding.maskView.width == 0 || binding.maskView.height == 0) return
binding.clockView.measure(
View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED
)
val clockWidth = binding.clockView.measuredWidth
val clockHeight = binding.clockView.measuredHeight
val maxX = binding.maskView.width - clockWidth
val maxY = binding.maskView.height - clockHeight
if (maxX > 0 && maxY > 0) {
val newX = random.nextInt(maxX.coerceAtLeast(1))
val newY = random.nextInt(maxY.coerceAtLeast(1))
binding.clockView.animate()
.x(newX.toFloat())
.y(newY.toFloat())
.setDuration(1000)
.setInterpolator(AccelerateDecelerateInterpolator())
.start()
}
handler.postDelayed(this, 30000)
}
}
handler.postDelayed(clockAnimationRunnable!!, 30000)
}
private fun stopClockAnimation(handler: Handler) {
clockAnimationRunnable?.let {
handler.removeCallbacks(it)
clockAnimationRunnable = null
}
}
fun destroy(handler: Handler) {
stopClockAnimation(handler)
currentAnimation?.cancel()
currentAnimation = null
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/MessageDispatcher.kt
================================================
package com.pengxh.daily.app.utils
import android.content.Context
import android.os.BatteryManager
import com.pengxh.daily.app.BuildConfig
import com.pengxh.daily.app.vm.MessageViewModel
import com.pengxh.kt.lite.extensions.timestampToDate
import com.pengxh.kt.lite.utils.SaveKeyValues
class MessageDispatcher(private val context: Context, private val viewModel: MessageViewModel) {
private val emailManager by lazy { EmailManager(context) }
private val batteryManager by lazy { context.getSystemService(BatteryManager::class.java) }
fun sendMessage(title: String, content: String) {
val messageTitle = SaveKeyValues.getValue(
Constant.MESSAGE_TITLE_KEY, "打卡结果通知"
) as String
val channelType = SaveKeyValues.getValue(Constant.CHANNEL_TYPE_KEY, -1) as Int
when (channelType) {
0 -> {
// 企业微信
val battery = batteryManager.getIntProperty(
BatteryManager.BATTERY_PROPERTY_CAPACITY
)
val content = buildString {
appendLine(title.ifBlank { messageTitle })
appendLine(content.ifBlank { "未监听到打卡成功的通知,请手动登录检查" })
appendLine("当前日期:${System.currentTimeMillis().timestampToDate()}")
appendLine("当前电量:${if (battery >= 0) "$battery%" else "未知"}")
append("版本号:${BuildConfig.VERSION_NAME}")
}
viewModel.sendMessage(content, {}, {}, {})
}
1 -> {
// QQ邮箱
val content = buildString {
append(content.ifBlank { "未监听到打卡成功的通知,请手动登录检查" })
}
emailManager.sendEmail(title.ifBlank { messageTitle }, content, false)
}
}
}
fun sendAttachmentMessage(title: String, content: String, filePath: String) {
val messageTitle =
SaveKeyValues.getValue(Constant.MESSAGE_TITLE_KEY, "打卡结果通知") as String
val battery = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
val date = System.currentTimeMillis().timestampToDate()
val channelType = SaveKeyValues.getValue(Constant.CHANNEL_TYPE_KEY, -1) as Int
when (channelType) {
0 -> {
// 企业微信(图文消息暂不支持)
// val content = """
// 标题:${title.ifBlank { messageTitle }}
// 内容:${content}
// 日期:$date
// 版本号:${BuildConfig.VERSION_NAME}
// 当前手机电量:${if (battery >= 0) "battery%" else "未知"}
// """.trimIndent()
viewModel.sendImageMessage(filePath, {}, {}, {})
}
1 -> {
// QQ邮箱
val content = buildString {
appendLine(content)
appendLine("当前日期:$date")
appendLine("当前电量:${if (battery >= 0) "$battery%" else "未知"}")
append("版本号:${BuildConfig.VERSION_NAME}")
}
emailManager.sendAttachmentEmail(
title.ifBlank { messageTitle },
content,
filePath,
false
)
}
}
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/ProjectionSession.kt
================================================
package com.pengxh.daily.app.utils
import android.media.projection.MediaProjection
import android.util.Log
import java.util.concurrent.atomic.AtomicReference
object ProjectionSession {
private const val kTag = "ProjectionSession"
enum class State {
IDLE,
ACTIVE,
NEED_AUTH
}
private val projectionRef = AtomicReference(null)
@Volatile
var state: State = State.IDLE
private set
fun setProjection(projection: MediaProjection) {
projectionRef.getAndSet(projection)?.let {
try {
it.stop()
} catch (e: Throwable) {
Log.w(kTag, "stop old projection failed", e)
}
}
state = State.ACTIVE
}
fun getProjection(): MediaProjection? = projectionRef.get()
fun markStoppedNeedAuth() {
state = State.NEED_AUTH
projectionRef.getAndSet(null)
}
fun clear() {
projectionRef.getAndSet(null)?.let {
try {
it.stop()
} catch (_: Throwable) {
// ignore
}
}
state = State.IDLE
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/TaskDataManager.kt
================================================
package com.pengxh.daily.app.utils
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import com.pengxh.daily.app.model.ExportDataModel
import com.pengxh.daily.app.sqlite.DatabaseWrapper
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean
import com.pengxh.kt.lite.utils.SaveKeyValues
class TaskDataManager() {
private val gson by lazy { Gson() }
fun importTasks(json: String): ImportResult {
return try {
val type = object : TypeToken() {}.type
val config = gson.fromJson(json, type)
val importedTasks = mutableListOf()
for (task in config.tasks) {
// 跳过已存在的任务时间点
if (!DatabaseWrapper.isTaskTimeExist(task.time)) {
DatabaseWrapper.insert(task)
importedTasks.add(task)
}
}
// 保存相关配置
saveConfiguration(config)
ImportResult.Success(importedTasks.size)
} catch (e: JsonSyntaxException) {
e.printStackTrace()
ImportResult.Error("导入失败,请确认导入的是正确的任务数据")
} catch (e: Exception) {
e.printStackTrace()
ImportResult.Error("导入失败:${e.message}")
}
}
private fun saveConfiguration(config: ExportDataModel) {
SaveKeyValues.putValue(Constant.MESSAGE_TITLE_KEY, config.messageTitle)
// 保存企业微信 Key
SaveKeyValues.putValue(Constant.WX_WEB_HOOK_KEY, config.wxKey)
val email = config.emailConfig
if (email != null) {
DatabaseWrapper.insertConfig(email.outbox, email.authCode, email.inbox)
}
SaveKeyValues.putValue(Constant.GESTURE_DETECTOR_KEY, config.isDetectGesture)
SaveKeyValues.putValue(Constant.BACK_TO_HOME_KEY, config.isBackToHome)
SaveKeyValues.putValue(Constant.RESET_TIME_KEY, config.resetTime)
SaveKeyValues.putValue(Constant.STAY_DD_TIMEOUT_KEY, config.overTime)
SaveKeyValues.putValue(Constant.TASK_COMMAND_KEY, config.command)
SaveKeyValues.putValue(Constant.TASK_AUTO_START_KEY, config.isAutoStart)
SaveKeyValues.putValue(Constant.RANDOM_TIME_KEY, config.isRandomTime)
SaveKeyValues.putValue(Constant.RANDOM_MINUTE_RANGE_KEY, config.timeRange)
}
sealed class ImportResult {
/** 导入成功,count 为成功导入的任务数量 */
data class Success(val count: Int) : ImportResult()
/** 导入失败,message 为错误信息 */
data class Error(val message: String) : ImportResult()
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/TaskResetReceiver.kt
================================================
package com.pengxh.daily.app.utils
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.pengxh.kt.lite.utils.SaveKeyValues
import org.greenrobot.eventbus.EventBus
class TaskResetReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val autoStart = SaveKeyValues.getValue(Constant.TASK_AUTO_START_KEY, true) as Boolean
if (autoStart) {
// 触发主界面重置任务
EventBus.getDefault().post(ApplicationEvent.ResetDailyTask)
}
// 重新注册明天同一时刻的 Alarm(循环触发)
val resetHour = SaveKeyValues.getValue(
Constant.RESET_TIME_KEY, Constant.DEFAULT_RESET_HOUR
) as Int
AlarmScheduler.schedule(context, resetHour)
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/TaskScheduler.kt
================================================
package com.pengxh.daily.app.utils
import android.os.Handler
import com.pengxh.daily.app.extensions.diffCurrent
import com.pengxh.daily.app.extensions.getTaskIndex
import com.pengxh.daily.app.service.CountDownTimerService
import com.pengxh.daily.app.sqlite.bean.DailyTaskBean
/**
* 任务调度器
*
* 职责:
* 1. 管理任务启动/停止状态
* 2. 执行每日任务调度逻辑
* 3. 协调倒计时服务和UI更新
*
* @param mainHandler 主线程Handler
* @param taskBeans 任务列表
* @param listener 任务状态回调
*/
class TaskScheduler(
private val mainHandler: Handler,
private val taskBeans: MutableList,
private val listener: TaskStateListener
) {
private var countDownTimerService: CountDownTimerService? = null
private var isTaskStarted = false
// 任务状态回调
interface TaskStateListener {
fun onTaskStarted()
fun onTaskStopped()
fun onTaskCompleted()
fun onTaskExecuting(taskIndex: Int, task: DailyTaskBean, realTime: String)
fun onTaskExecutionError(message: String)
}
fun setCountDownTimerService(service: CountDownTimerService?) {
this.countDownTimerService = service
}
fun isTaskStarted(): Boolean = isTaskStarted
/**
* 启动任务
*/
fun startTask() {
LogFileManager.writeLog("开始执行每日任务")
// 更新状态标志
isTaskStarted = true
// 启动任务调度,先移除所有未执行的 Runnable,避免重复投递
mainHandler.removeCallbacks(dailyTaskRunnable)
mainHandler.post(dailyTaskRunnable)
// 通知状态变更
listener.onTaskStarted()
}
/**
* 停止任务
*/
fun stopTask() {
LogFileManager.writeLog("停止执行每日任务")
isTaskStarted = false
// 取消任务调度
mainHandler.removeCallbacks(dailyTaskRunnable)
// 取消服务中的倒计时
countDownTimerService?.cancelCountDown()
// 通知状态变更
listener.onTaskStopped()
}
/**
* 取消超时定时器并执行下一个任务
* 此方法由外部调用,在收到打卡成功广播时
*/
fun executeNextTask() {
LogFileManager.writeLog("执行下一个任务")
// 先移除所有未执行的 Runnable,避免重复投递
mainHandler.removeCallbacks(dailyTaskRunnable)
mainHandler.post(dailyTaskRunnable)
}
/**
* 当日串行任务Runnable
* 负责按顺序执行每日任务
*/
private val dailyTaskRunnable = object : Runnable {
override fun run() {
try {
val index = taskBeans.getTaskIndex()
if (index == -1) {
LogFileManager.writeLog("今日任务已全部执行完毕")
mainHandler.removeCallbacks(this)
// 通知任务完成
listener.onTaskCompleted()
// 更新服务状态
countDownTimerService?.updateDailyTaskState()
return
}
// 二次验证索引是否在有效范围内
if (index < 0 || index >= taskBeans.size) {
val errorMsg = "任务索引超出范围: $index, 数组大小: ${taskBeans.size}"
LogFileManager.writeLog(errorMsg)
listener.onTaskExecutionError(errorMsg)
return
}
LogFileManager.writeLog("执行任务,任务index是: $index,时间是: ${taskBeans[index].time}")
val task = taskBeans[index]
val taskIndex = index + 1
// 计算时间差
val (realTime, timeSeconds) = task.diffCurrent()
// 通知UI更新
listener.onTaskExecuting(taskIndex, task, realTime)
// 启动倒计时
countDownTimerService?.startCountDown(taskIndex, timeSeconds)
} catch (e: IndexOutOfBoundsException) {
val errorMsg = "任务数组访问越界: ${e.message}"
LogFileManager.writeLog(errorMsg)
listener.onTaskExecutionError(errorMsg)
} catch (e: Exception) {
val errorMsg = "执行任务时发生异常: ${e.message}"
LogFileManager.writeLog(errorMsg)
listener.onTaskExecutionError(errorMsg)
}
}
}
fun destroy() {
mainHandler.removeCallbacks(dailyTaskRunnable)
countDownTimerService = null
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/TimeKit.kt
================================================
package com.pengxh.daily.app.utils
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object TimeKit {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.CHINA)
fun getTodayDate(): String {
return dateFormat.format(Date())
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/TimeoutTimerManager.kt
================================================
package com.pengxh.daily.app.utils
import android.os.CountDownTimer
import android.os.Handler
import com.pengxh.kt.lite.utils.SaveKeyValues
import org.greenrobot.eventbus.EventBus
/**
* 超时定时器管理器
*
* 职责:
* 1. 管理打卡超时定时器的生命周期
* 2. 向悬浮窗广播倒计时更新
* 3. 处理超时后的逻辑(返回主界面、发送异常邮件)
* 4. 提供定时器取消接口
*
* @param mainHandler 主线程Handler
*/
class TimeoutTimerManager(private val mainHandler: Handler) {
private var timeoutTimer: CountDownTimer? = null
private var timeoutSeconds: Int = 0
private var hasCaptured = false
/**
* 启动超时定时器
*
* @param onTimeout 超时回调,当倒计时结束时触发
*/
fun startTimeoutTimer(onTimeout: () -> Unit) {
hasCaptured = false
// 取消之前的定时器,防止重复创建
cancelTimeoutTimer()
// 获取超时时长配置(单位:秒)
timeoutSeconds = try {
val value = SaveKeyValues.getValue(
Constant.STAY_DD_TIMEOUT_KEY, Constant.DEFAULT_OVER_TIME
)
(value as? Int) ?: Constant.DEFAULT_OVER_TIME
} catch (_: Exception) {
Constant.DEFAULT_OVER_TIME
}
timeoutTimer = object : CountDownTimer(timeoutSeconds * 1000L, 1000) {
override fun onTick(millisUntilFinished: Long) {
val tick = (millisUntilFinished / 1000).toInt()
// 更新悬浮窗倒计时
EventBus.getDefault().post(ApplicationEvent.UpdateFloatingViewTime(tick))
// 启用截屏
val resultSource = SaveKeyValues.getValue(Constant.RESULT_SOURCE_KEY, 0) as Int
if (resultSource == 1) {
if (tick <= 3 && !hasCaptured) {
hasCaptured = true
EventBus.getDefault().post(ApplicationEvent.CaptureScreen)
}
}
}
override fun onFinish() {
mainHandler.post {
onTimeout()
}
timeoutTimer = null
hasCaptured = false
}
}
timeoutTimer?.start()
}
/**
* 取消超时定时器
*/
fun cancelTimeoutTimer() {
timeoutTimer?.cancel()
timeoutTimer = null
}
/**
* 销毁资源
*/
fun destroy() {
cancelTimeoutTimer()
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/utils/WatermarkDrawable.kt
================================================
package com.pengxh.daily.app.utils
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import androidx.core.graphics.toColorInt
import androidx.core.graphics.withRotation
import com.pengxh.kt.lite.extensions.sp2px
import kotlin.math.sqrt
class WatermarkDrawable(private val context: Context, private val watermark: String) : Drawable() {
private val paint by lazy {
Paint().apply {
textSize = 15f.sp2px(context)
color = "#FFDFDFDF".toColorInt()
}
}
override fun draw(canvas: Canvas) {
val width = bounds.right
val height = bounds.bottom
canvas.drawColor("#40F3F5F9".toColorInt())
canvas.withRotation(-30f) {
val textWidth = paint.measureText(watermark)
val textHeight = paint.fontSpacing.toInt()
// 使用屏幕对角线长度作为绘制范围,确保旋转后也能完全覆盖
val diagonal = sqrt((width * width + height * height).toDouble()).toFloat()
// 垂直方向:从负值开始,确保左上角也有水印
var y = -diagonal
var rowIndex = 0
while (y < diagonal) {
// 水平方向:交错排列
val offset = if (rowIndex % 2 == 0) 0f else textWidth / 3
var x = -diagonal + offset
// 绘制这一行的水印
val horizontalSpacing = diagonal * 0.5f
repeat((0..2).count()) {
canvas.drawText(watermark, x, y, paint)
x += horizontalSpacing
}
y += textHeight + 200
rowIndex++
}
}
}
override fun setAlpha(alpha: Int) {
}
override fun getOpacity(): Int {
return PixelFormat.UNKNOWN
}
override fun setColorFilter(colorFilter: ColorFilter?) {
}
}
================================================
FILE: app/src/main/java/com/pengxh/daily/app/vm/MessageViewModel.kt
================================================
package com.pengxh.daily.app.vm
import androidx.lifecycle.ViewModel
import com.pengxh.daily.app.extensions.getResponseHeader
import com.pengxh.daily.app.retrofit.RetrofitServiceManager
import com.pengxh.kt.lite.extensions.launch
class MessageViewModel : ViewModel() {
fun sendMessage(
content: String,
onLoading: () -> Unit,
onSuccess: () -> Unit,
onFailed: (String) -> Unit
) = launch({
onLoading()
val response = RetrofitServiceManager.sendMessage(content)
val header = response.getResponseHeader()
if (header.first == 0) {
onSuccess()
} else {
onFailed(header.second)
}
}, {
it.printStackTrace()
onFailed(it.message ?: "Unknown error")
})
fun sendImageMessage(
imagePath: String,
onLoading: () -> Unit,
onSuccess: () -> Unit,
onFailed: (String) -> Unit
) = launch({
onLoading()
val response = RetrofitServiceManager.sendImageMessage(imagePath)
val header = response.getResponseHeader()
if (header.first == 0) {
onSuccess()
} else {
onFailed(header.second)
}
}, {
it.printStackTrace()
onFailed(it.message ?: "Unknown error")
})
}
================================================
FILE: app/src/main/res/drawable/bg_solid_layout_white_16.xml
================================================
================================================
FILE: app/src/main/res/drawable/divider_gradient.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_arrow_right.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_clear.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_ding_ding.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_fei_shu.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_menu_add.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_menu_settings.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_sand.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_title_right_black.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_wei_xin.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_message_channel.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_question_and_answer.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_settings.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_task_config.xml
================================================
================================================
FILE: app/src/main/res/layout/bottom_sheet_layout_select_time.xml
================================================
================================================
FILE: app/src/main/res/layout/item_app_rv_l.xml
================================================
================================================
FILE: app/src/main/res/layout/item_daily_task_rv_l.xml
================================================
================================================
FILE: app/src/main/res/layout/item_notice_rv_l.xml
================================================
================================================
FILE: app/src/main/res/layout/item_q_a_rv_l.xml
================================================
================================================
FILE: app/src/main/res/layout/item_task_rv_g.xml
================================================
================================================
FILE: app/src/main/res/layout/window_floating.xml
================================================
================================================
FILE: app/src/main/res/menu/email_config_top_bar_menu.xml
================================================
================================================
FILE: app/src/main/res/menu/main_top_bar_menu.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#3399FF
#4DDC64
#F1F1F1
#CCCCCC
#424242
#000000
#FFFFFF
#FF0000
#D3D3D3
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
10sp
12sp
14sp
16sp
18sp
20sp
22sp
4dp
8dp
12dp
16dp
24dp
32dp
40dp
48dp
56dp
64dp
72dp
1dp
1px
48dp
60dp
================================================
FILE: app/src/main/res/values/ic_launcher_background.xml
================================================
#000000
================================================
FILE: app/src/main/res/values/strings.xml
================================================
DailyTask
DailyTask通知监听服务
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/values/themes.xml
================================================
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.11.1" apply false
id 'org.jetbrains.kotlin.android' version '2.3.20' apply false
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Wed Dec 25 09:13:58 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.useDeprecatedNdk=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: settings.gradle
================================================
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
maven { url = uri("https://maven.aliyun.com/repository/public") }
}
}
rootProject.name = "DailyTask"
include ':app'