├── .gitignore ├── ColorDetectTest.js ├── LICENSE ├── README.md ├── config.js ├── core ├── Ant_forest.js ├── FriendListScanner.js └── ImgBasedFriendListScanner.js ├── lib ├── Automator.js ├── CommonFunction.js ├── DateCompare.js ├── DateUtil.js ├── FileUtils.js ├── FloatyUtil.js ├── LogUtils.js ├── RunningQueueDispatcher.js ├── Timers.js ├── TryRequestScreenCapture.js ├── Unlock.js ├── WidgetUtils.js ├── autojs-tools.dex ├── mock │ └── mockFunctions.js ├── scheduler.js └── test.js ├── main.js ├── project.json ├── test ├── BitCheckTest.js ├── ColorDetectTest.js ├── FloatyTest.js ├── FloatyTest2.js ├── FloatyTest3.js ├── TestBit.js └── testScrollUpAndDown.js ├── unit ├── FriendListGet.js ├── queueTest │ ├── Task1.js │ ├── Task2.js │ ├── Task3.js │ └── TaskTestManager.js ├── 功能测试-任务队列测试.js ├── 功能测试-查看运行队列.js ├── 功能测试-清空任务队列.js ├── 功能测试.js ├── 展示今日收集能量信息.js ├── 校验保护罩.js ├── 浇水三次.js ├── 能量收集统计.js ├── 自定义1永不停止.js └── 自定义2计时停止.js ├── update ├── glb.dex ├── 检测更新.js └── 说明-重要.txt └── version.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.zip 3 | -------------------------------------------------------------------------------- /ColorDetectTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-12-04 18:09:36 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-06 15:25:49 6 | * @Description: 7 | */ 8 | runtime.loadDex('./lib/autojs-tools.dex') 9 | let FloatyInstance = require('./lib/FloatyUtil.js') 10 | let ImgBasedFriendListScanner = require('./core/ImgBasedFriendListScanner') 11 | 12 | FloatyInstance.init() 13 | requestScreenCapture(false) 14 | let scanner = new ImgBasedFriendListScanner() 15 | scanner.start() 16 | 17 | sleep(2000) 18 | log('再见') 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 当前仓库停止更新,请移步新仓库 2 | - [新仓库传送门](https://github.com/TonyJiangWJ/Ant-Forest) 3 | - 因为和主分支差异较大,不准备在这个fork的分支上继续开发了,现在将仓库复制了一份 独立开发 4 | # 其他脚本 5 | - [蚂蚁庄园传送门](https://github.com/TonyJiangWJ/Ant-Manor) 6 | # 简介 7 | 8 | 基于 Autojs 的蚂蚁森林自动收能量脚本,采用 4.1.1 Alpha 版本开发。解锁模块参考自:https://github.com/e1399579/autojs 9 | 10 | - 经过测试小米 mix2s 可以使用 4.0.4alpha 版本,4.0.5alpha 会报错 11 | - 找到最新版了 经过测试可以执行。建议下载该版本,可以直接设置定时任务,而不需要通过脚本中的定时启动方式。自备梯子:[AutoJs 4.1.1 alpha2 下载](https://www.dropbox.com/s/pe3w53k0fugo1fa/Autojs%204.1.1%20Alpha2.apk?dl=0) 12 | 13 | ## 更新记录 14 | 15 | - 本项目从https://github.com/Nick-Hopps/Ant-Forest-autoscript fork 而来,但是经过了各种改动,和原版功能差异较大 16 | - 历史版本下载可前往[RELEASES 页面](https://github.com/TonyJiangWJ/Ant-Forest-autoscript/releases) 17 | 18 | # 使用 19 | 20 | - 下载安装 [AutoJs 4.1.1 alpha2 下载](https://www.dropbox.com/s/pe3w53k0fugo1fa/Autojs%204.1.1%20Alpha2.apk?dl=0) 之后把整个脚本项目放进 **"/sdcard/脚本/"** 文件夹下面。打开软件后下拉刷新,然后运行项目或者 main 即可。 21 | 22 | # 功能 23 | 24 | - 自动匹配不同系统下自动化的方式,安卓 7 及以上通过无障碍服务模拟操作,以下版本通过 root 权限模拟操作; 25 | - 自动识别屏幕锁定方式并根据配置的密码解锁,支持图形解锁,PIN 解锁,混合密码解锁; 26 | - 识别自己能量球的倒计时,和好友列表中的倒计时做对比,取最小值作为下次收取的等待时间; 27 | - 识别好友能量罩,下一次收取时跳过开启能量罩的好友; 28 | - 默认使用倒计时收取,可通过配置打开循环收取; 29 | - 根据设置选择是否帮助好友收取能量; 30 | - 根据白名单实现不收取特定好友能量; 31 | - 脚本运行时可以显示悬浮窗展示当前状态 32 | - 开始收集的时候按音量减可以延迟五分钟再执行,适合需要使用手机的时候使用 33 | - 收取完毕后悬浮框显示收取的能量数量。 34 | - 可以自动打开无障碍,需要配合adb赋权 `adb shell pm grant org.autojs.autojs android.permission.WRITE_SECURE_SETTINGS` 35 | - 可以自动打开脚本执行之前的APP 增强体验,获取失败时通过返回最小化支付宝实现同样效果 36 | - 脚本更新 可以执行`update/检测更新.js` 37 | 38 | # 配置 39 | 40 | 打开 config.js 后可以看到如下配置: 41 | 42 | ```javascript 43 | // 执行配置 44 | var default_config = { 45 | color_offset: 50, 46 | password: '', 47 | help_friend: true, 48 | is_cycle: false, 49 | cycle_times: 10, 50 | // 是否永不停止,即倒计时信息不存在时 睡眠reactive_time之后重新开始收集 51 | never_stop: false, 52 | // 重新激活等待时间 单位分钟 53 | reactive_time: 60, 54 | timeout_unlock: 1000, 55 | timeout_findOne: 1000, 56 | // 检测是否存在的默认超时时间,8秒 57 | timeout_existing: 8000, 58 | max_collect_repeat: 20, 59 | // 是否显示状态栏的悬浮窗,避免遮挡,悬浮窗位置可以通过后两项配置修改 min_floaty_x[y] 60 | show_small_floaty: true, 61 | // 设置悬浮窗的位置,避免遮挡时间之类的 或者被刘海挡住,一般异形屏的min_floaty_y值需要设为负值 62 | min_floaty_x: 150, 63 | min_floaty_y: 20, 64 | // mini悬浮窗字体颜色 当前默认为亮绿色 65 | min_floaty_color: '#00FF00', 66 | // 计时模式下 收集能量的最大等待时间 分钟 67 | max_collect_wait_time: 60, 68 | // 白名单列表,即不去收取他们的能量 69 | white_list: [], 70 | // 是否跳过低于五克的能量,避免频繁偷别人的 这个其实不好用 不建议开启 71 | skip_five: false, 72 | // 是否显示调试日志信息 73 | show_debug_log: true, 74 | // 是否在收集完成后根据收集前状态判断是否锁屏,非ROOT设备通过下拉状态栏中的锁屏按钮实现 需要配置锁屏按钮位置,仅仅测试MIUI的 其他系统可能没法用 75 | // 可以自己研究研究之后 修改Automator.js中的lockScreen方法 76 | auto_lock: false, 77 | // 配置锁屏按钮位置 78 | lock_x: 150, 79 | lock_y: 970, 80 | // 是否需要检测录屏弹窗 81 | request_capture_permission: true 82 | } 83 | /** 84 | * 非可视化控制的配置 通过手动修改config.js来实现配置 85 | */ 86 | let no_gui_config = { 87 | // 只在AutoJS中能打开,定时不能打开时 尝试开启这个 设为true 88 | fuck_miui11: false, 89 | // 单脚本模式 是否只运行一个脚本 不会同时使用其他的 开启单脚本模式 会取消任务队列的功能。 90 | // 比如同时使用蚂蚁庄园 则保持默认 false 否则设置为true 无视其他运行中的脚本 91 | single_script: false, 92 | // 设备高度 正常情况下device.height可以获取到 93 | // deviceHeight: 2160, 94 | // 预加载超时,其实可以不用管这个 该值会在运行中自动配置合适的时间 @deprecated 新版蚂蚁森林没法使用 95 | timeoutLoadFriendList: 6000, 96 | // 这个用于控制列表滑动是否稳定 不用去修改它 97 | friendListStableCount: 3, 98 | // 底部高度,比如有虚拟按键就需要修改这个值 设置比虚拟按键高度高就可以了 99 | bottomHeight: 200, 100 | // 是否使用模拟的滑动,如果滑动有问题开启这个 当前默认关闭 经常有人手机上有虚拟按键 然后又不看文档注释的 101 | useCustomScrollDown: false, 102 | // 排行榜列表下滑速度 100毫秒 仅仅针对useCustomScrollDown=true的情况 103 | scrollDownSpeed: 100, 104 | // 配置帮助收取能量球的颜色,用于查找帮助收取的能量球 105 | helpBallColors: ['#f99236', '#f7af70'], 106 | // 是否保存日志文件,如果设置为保存,则日志文件会按时间分片备份在logback/文件夹下 107 | saveLogFile: true, 108 | // 是否开启自动浇水 每日收集某个好友达到下一个阈值之后会进行浇水 109 | wateringBack: true, 110 | // 浇水阈值40克 111 | wateringThreshold: 40, 112 | // 配置不浇水的黑名单 113 | wateringBlackList: [], 114 | // 是否根据当前锁屏状态来设置屏幕亮度,当锁屏状态下启动时 设置为最低亮度,结束后设置成自动亮度 115 | autoSetBrightness: false, 116 | // 延迟启动时延 5秒 悬浮窗中进行的倒计时时间 117 | delayStartTime: 5000, 118 | // 收集完一轮后不驻留悬浮窗 119 | notLingeringFloatWindow: false 120 | } 121 | 122 | // UI配置 针对多语言环境 英文界面替换成相应的英文内容即可 建议还是用中文界面比较好 123 | var ui_config = { 124 | home_ui_content: '背包|通知|攻略', 125 | friend_home_ui_content: '浇水|发消息', 126 | // 废弃 127 | friend_list_ui_content: '(周|总)排行榜', 128 | // 用于判断是否在好友排行榜 129 | friend_list_id: '.*react-content.*', 130 | // 查看更多好友的按钮 131 | enter_friend_list_ui_content: '查看更多好友', 132 | no_more_ui_content: '没有更多了', 133 | load_more_ui_content: '查看更多', 134 | watering_widget_content: '浇水', 135 | using_protect_content: '使用了保护罩', 136 | collectable_energy_ball_content: /收集能量\d+克/ 137 | } 138 | 139 | ``` 140 | 141 | 其中: 142 | 143 | - `default_config`中的配置可以运行 configGui.js 来可视化修改,其他两个配置`no_gui_config`和`ui_config`需要通过修改 config.js 文件中的值 144 | 145 | # 添加解锁设备 146 | 147 | 在 Unlock.js 中,按照以下格式扩展: 148 | 149 | ```javascript 150 | var Devices = { 151 | device_1: function(obj) { 152 | this.__proto__ = obj; 153 | 154 | this.unlock = function(password) { 155 | if (typeof password !== "string") throw new Error("密码应为字符串!"); 156 | 157 | // 此处为解锁的代码 158 | 159 | return this.check_unlock(); 160 | } 161 | }, 162 | device_2: function(obj) { 163 | ... 164 | }, 165 | device_3: function(obj) { 166 | ... 167 | } 168 | } 169 | ``` 170 | 171 | 上述所示为最简单的解锁模板,也可以参考 Unlock.js 默认多解锁方式的代码进行修改。 172 | 173 | 然后在下方的 MyDevice 中设置解锁设备: 174 | 175 | ```javascript 176 | var MyDevice = Devices.device_1 177 | ``` 178 | 179 | # 注意事项 180 | 181 | 解锁仅支持: 182 | 183 | - 具有 ROOT 权限的安卓 5.0 及以上版本 184 | - 没有 ROOT 权限的安卓 7.0 及以上版本 185 | 186 | # 目前存在的问题 187 | 188 | - 功能性问题暂无,兼容性问题有部分MIUI版本可能会死机 暂时没有找到合适的解决方法 可以先增加配置 `fuck_miui11: true`,放在no_gui_config中即可 189 | -------------------------------------------------------------------------------- /core/Ant_forest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: NickHopps 3 | * @Last Modified by: TonyJiangWJ 4 | * @Last Modified time: 2019-12-10 13:33:54 5 | * @Description: 蚂蚁森林操作集 6 | */ 7 | let _widgetUtils = typeof WidgetUtils === 'undefined' ? require('../lib/WidgetUtils.js') : WidgetUtils 8 | let automator = require('../lib/Automator.js') 9 | let _commonFunctions = typeof commonFunctions === 'undefined' ? require('../lib/CommonFunction.js') : commonFunctions 10 | let _runningQueueDispatcher = typeof runningQueueDispatcher === 'undefined' ? require('./RunningQueueDispatcher.js') : runningQueueDispatcher 11 | let _config = typeof config === 'undefined' ? require('../config.js').config : config 12 | let FriendListScanner = require('./FriendListScanner.js') 13 | 14 | function Ant_forest () { 15 | const _package_name = 'com.eg.android.AlipayGphone' 16 | 17 | let _pre_energy = 0, // 记录收取前能量值 18 | _post_energy = 0, // 记录收取后能量值 19 | _timestamp = 0, // 记录获取自身能量倒计时 20 | _min_countdown = 0, // 最小可收取倒计时 21 | _current_time = 0, // 当前收集次数 22 | _fisrt_running = true, // 是否第一次进入蚂蚁森林 23 | _has_next = true, // 是否下一次运行 24 | _collect_any = false, // 收集过能量 25 | _re_try = 0, 26 | _lost_someone = false // 是否漏收, 27 | _friends_min_countdown = 0 28 | /*********************** 29 | * 综合操作 30 | ***********************/ 31 | 32 | // 进入蚂蚁森林主页 33 | const startApp = function () { 34 | _commonFunctions.launchPackage(_package_name, false) 35 | app.startActivity({ 36 | action: 'VIEW', 37 | data: 'alipays://platformapi/startapp?appId=60000002', 38 | packageName: _package_name 39 | }) 40 | } 41 | 42 | /** 43 | * 关闭活动弹窗 44 | */ 45 | const clearPopup = function () { 46 | // 合种/添加快捷方式提醒 47 | threads.start(function () { 48 | let floty = idEndsWith('J_pop_treedialog_close').findOne( 49 | _config.timeout_findOne 50 | ) 51 | if (floty) { 52 | floty.click() 53 | } 54 | }) 55 | threads.start(function () { 56 | 57 | let buttons = className('android.widget.Button') 58 | .desc('关闭').findOne( 59 | _config.timeout_findOne 60 | ) 61 | if (buttons) { 62 | buttons.click() 63 | } 64 | }) 65 | threads.start(function () { 66 | let floty = descEndsWith('关闭蒙层').findOne(_config.timeout_findOne) 67 | if (floty) { 68 | floty.click() 69 | } 70 | floty = textEndsWith('关闭蒙层').findOne(_config.timeout_findOne) 71 | if (floty) { 72 | floty.click() 73 | } 74 | }) 75 | debugInfo('关闭蒙层成功') 76 | } 77 | 78 | // 显示文字悬浮窗 79 | const showFloaty = function (text) { 80 | _commonFunctions.closeFloatyWindow() 81 | let window = floaty.window( 82 | 83 | 84 | 93 | 102 | 110 | × 111 | 112 | 113 | 114 | 115 | ) 116 | window.stop.on('click', () => { 117 | exit() 118 | }) 119 | logInfo(text) 120 | ui.run(function () { 121 | window.log.text(text) 122 | }) 123 | // 30秒后关闭,防止立即停止 124 | setTimeout(() => { exit() }, 1000 * 30) 125 | } 126 | 127 | /*********************** 128 | * 构建下次运行 129 | ***********************/ 130 | 131 | // 异步获取 toast 内容 132 | const getToastAsync = function (filter, limit, exec) { 133 | filter = typeof filter == null ? '' : filter 134 | let lock = threads.lock() 135 | let complete = lock.newCondition() 136 | let result = [] 137 | lock.lock() 138 | 139 | // 在新线程中开启监听 140 | let thread = threads.start(function () { 141 | try { 142 | lock.lock() 143 | let temp = [] 144 | let counter = 0 145 | let toastDone = false 146 | let startTimestamp = new Date().getTime() 147 | // 监控 toast 148 | events.onToast(function (toast) { 149 | if (toastDone) { 150 | return 151 | } 152 | if ( 153 | toast && 154 | toast.getPackageName() && 155 | toast.getPackageName().indexOf(filter) >= 0 156 | ) { 157 | counter++ 158 | temp.push(toast.getText()) 159 | if (counter == limit) { 160 | logInfo('正常获取toast信息' + temp) 161 | toastDone = true 162 | } else if (new Date().getTime() - startTimestamp > 10000) { 163 | warnInfo('等待超过十秒秒钟,直接返回结果') 164 | toastDone = true 165 | } 166 | } else { 167 | errorInfo('无法获取toast内容,直接返回[]') 168 | toastDone = true 169 | } 170 | }) 171 | // 触发 toast 172 | exec() 173 | let count = 10 174 | while (count-- > 0 && !toastDone) { 175 | sleep(1000) 176 | } 177 | if (!toastDone) { 178 | warnInfo('获取toast超时释放锁') 179 | } 180 | debugInfo('temp' + temp) 181 | result = temp 182 | } finally { 183 | complete.signal() 184 | lock.unlock() 185 | } 186 | }) 187 | // 获取结果 188 | debugInfo('阻塞等待toast结果') 189 | complete.await() 190 | debugInfo('阻塞等待结束,等待锁释放') 191 | lock.unlock() 192 | debugInfo('获取toast结果成功:' + result) 193 | thread.interrupt() 194 | return result 195 | } 196 | 197 | // 获取自己的能量球中可收取倒计时的最小值 198 | const getMinCountdownOwn = function () { 199 | let target 200 | if (className('Button').descMatches(/\s/).exists()) { 201 | target = className('Button') 202 | .descMatches(/\s/) 203 | .filter(function (obj) { 204 | return obj.bounds().height() / obj.bounds().width() > 1.05 205 | }) 206 | } else if (className('Button').textMatches(/\s/).exists()) { 207 | target = className('Button') 208 | .textMatches(/\s/) 209 | .filter(function (obj) { 210 | return obj.bounds().height() / obj.bounds().width() > 1.05 211 | }) 212 | } 213 | if (target && target.exists()) { 214 | let ball = target.untilFind() 215 | let temp = [] 216 | debugInfo('待收取球数' + ball.length) 217 | let toasts = getToastAsync(_package_name, ball.length >= 2 ? 2 : ball.length, function () { 218 | // 只需要点击两个球就够了 219 | for (let i = 0; i < ball.length && i < 2; i++) { 220 | let countDownBall = ball[i] 221 | automator.clickCenter(countDownBall) 222 | sleep(500) 223 | } 224 | }) 225 | toasts.forEach(function (toast) { 226 | let countdown = toast.match(/\d+/g) 227 | if (countdown !== null && countdown.length >= 2) { 228 | temp.push(countdown[0] * 60 - -countdown[1]) 229 | } else { 230 | errorInfo('获取倒计时错误:' + countdown) 231 | } 232 | }) 233 | _min_countdown = Math.min.apply(null, temp) 234 | } else { 235 | _min_countdown = null 236 | logInfo('无可收取能量') 237 | } 238 | _timestamp = new Date() 239 | } 240 | 241 | // 确定下一次收取倒计时 242 | const getMinCountdown = function () { 243 | let countDownNow = calculateMinCountdown() 244 | // 如果有收集过能量,那么先返回主页在进入排行榜,以获取最新的倒计时信息,避免收集过的倒计时信息不刷新,此过程可能导致执行过慢 245 | if (_collect_any) { 246 | /** TODO 暂时屏蔽 247 | if (!isFinite(countDownNow) || countDownNow >= 2) { 248 | debugInfo('收集过能量,重新获取倒计时列表,原倒计时时间:[' + countDownNow + ']分') 249 | automator.clickBack() 250 | _widgetUtils.homePageWaiting() 251 | automator.enterFriendList() 252 | _widgetUtils.friendListWaiting() 253 | _widgetUtils.quickScrollDown() 254 | sleep(100) 255 | // 再次获取倒计时数据 256 | let newCountDown = calculateMinCountdown(countDownNow, new Date()) 257 | debugInfo('第二次获取倒计时时间:[' + newCountDown + ']分') 258 | if (isFinite(countDownNow)) { 259 | countDownNow = (isFinite(newCountDown) && newCountDown < countDownNow) ? newCountDown : countDownNow 260 | } else { 261 | countDownNow = newCountDown 262 | } 263 | } else { 264 | debugInfo('当前倒计时时间短,无需再次获取') 265 | } 266 | */ 267 | } else { 268 | debugInfo('未收集能量直接获取倒计时列表') 269 | } 270 | _min_countdown = isFinite(countDownNow) ? countDownNow : _min_countdown 271 | } 272 | 273 | const calculateMinCountdown = function (lastMin, lastTimestamp) { 274 | let temp = [] 275 | if (_friends_min_countdown && isFinite(_friends_min_countdown)) { 276 | temp.push(_friends_min_countdown) 277 | } 278 | if (_min_countdown && isFinite(_min_countdown) && _timestamp instanceof Date) { 279 | debugInfo('已记录自身倒计时:' + _min_countdown + '分') 280 | let passedTime = Math.round((new Date() - _timestamp) / 60000) 281 | let countdown_own = _min_countdown - passedTime 282 | debugInfo('本次收集经过了:[' + passedTime + ']分,最终记录自身倒计时:[' + countdown_own + ']分') 283 | countdown_own >= 0 ? temp.push(countdown_own) : temp.push(0) 284 | } 285 | if (isFinite(lastMin) && lastTimestamp instanceof Date) { 286 | debugInfo('上轮获取倒计时[' + lastMin + ']分') 287 | let passedTime = Math.round((new Date() - lastTimestamp) / 60000) 288 | lastMin = lastMin - passedTime 289 | debugInfo('重新获取倒计时经过了:[' + passedTime + ']分,最终记录上轮倒计时:[' + lastMin + ']分') 290 | lastMin >= 0 ? temp.push(lastMin) : temp.push(0) 291 | } 292 | let friCountDowmContainer = _widgetUtils.widgetGetAll('\\d+’', null, true) 293 | let peekCountdownContainer = function (container) { 294 | if (container) { 295 | return _commonFunctions.formatString('倒计时数据总长度:{} 文本属性来自[{}]', container.target.length, (container.isDesc ? 'desc' : 'text')) 296 | } else { 297 | return null 298 | } 299 | } 300 | debugInfo('get \\d+’ container:' + peekCountdownContainer(friCountDowmContainer)) 301 | if (friCountDowmContainer) { 302 | let isDesc = friCountDowmContainer.isDesc 303 | friCountDowmContainer.target.forEach(function (countdown) { 304 | let countdown_fri = null 305 | if (isDesc) { 306 | countdown_fri = parseInt(countdown.desc().match(/\d+/)) 307 | } else if (countdown.text()) { 308 | countdown_fri = parseInt(countdown.text().match(/\d+/)) 309 | } 310 | if (countdown_fri) { 311 | temp.push(countdown_fri) 312 | } 313 | }) 314 | } 315 | if (temp.length === 0) { 316 | return 317 | } 318 | temp = temp.filter(i => isFinite(i)) 319 | let min = Math.min.apply(null, temp) 320 | min = isFinite(min) ? min : undefined 321 | debugInfo('获取倒计时最小值:[' + min + ']分') 322 | return min 323 | } 324 | 325 | 326 | // 构建下一次运行 327 | const generateNext = function () { 328 | // 循环模式,判断循环次数 329 | if (_config.is_cycle) { 330 | if (_current_time < _config.cycle_times) { 331 | _has_next = true 332 | } else { 333 | logInfo("达到最大循环次数") 334 | _has_next = false 335 | } 336 | } else { 337 | // 永不终止模式,判断倒计时不存在,直接等待配置的激活时间 338 | if (_config.never_stop) { 339 | if (_commonFunctions.isEmpty(_min_countdown) || _min_countdown > _config.reactive_time) { 340 | _min_countdown = _config.reactive_time || 60 341 | } 342 | _has_next = true 343 | return 344 | } 345 | // 计时模式 超过最大循环次数 退出执行 346 | if (_current_time > _config.max_collect_repeat) { 347 | _has_next = false 348 | logInfo("达到最大循环次数") 349 | return 350 | } 351 | // 计时模式,超过最大等待时间 退出执行 352 | if ( 353 | _min_countdown != null && 354 | _min_countdown <= _config.max_collect_wait_time 355 | ) { 356 | _has_next = true 357 | } else { 358 | logInfo(_min_countdown + "超过最大等待时间") 359 | _has_next = false 360 | } 361 | } 362 | } 363 | 364 | /*********************** 365 | * 记录能量 366 | ***********************/ 367 | 368 | // 记录当前能量 369 | const getCurrentEnergy = function () { 370 | let currentEnergyWidgetContainer = _widgetUtils.widgetGetOne('\\d+g', null, true) 371 | let currentEnergy = undefined 372 | if (currentEnergyWidgetContainer) { 373 | let target = currentEnergyWidgetContainer.target 374 | let isDesc = currentEnergyWidgetContainer.isDesc 375 | if (isDesc) { 376 | currentEnergy = parseInt(target.desc().match(/\d+/)) 377 | } else { 378 | currentEnergy = parseInt(target.text().match(/\d+/)) 379 | } 380 | } 381 | if (currentEnergy) { 382 | // 存储能量值数据 383 | _commonFunctions.storeEnergy(currentEnergy) 384 | } 385 | return currentEnergy 386 | } 387 | 388 | // 记录初始能量值 389 | const getPreEnergy = function () { 390 | let currentEnergy = getCurrentEnergy() 391 | if (_fisrt_running && _has_next) { 392 | _pre_energy = currentEnergy 393 | _commonFunctions.persistHistoryEnergy(currentEnergy) 394 | logInfo('当前能量:' + currentEnergy) 395 | } 396 | showCollectSummaryFloaty() 397 | } 398 | 399 | const showCollectSummaryFloaty = function (increased) { 400 | if (_config.is_cycle) { 401 | _commonFunctions.showCollectSummaryFloaty0(_post_energy - _pre_energy, _current_time, increased) 402 | } else { 403 | _commonFunctions.showCollectSummaryFloaty0(null, null, increased) 404 | } 405 | } 406 | 407 | // 记录最终能量值 408 | const getPostEnergy = function () { 409 | automator.clickBack() 410 | _widgetUtils.homePageWaiting() 411 | // 等待能量值稳定 412 | sleep(500) 413 | _post_energy = getCurrentEnergy() 414 | logInfo('当前能量:' + _post_energy) 415 | _commonFunctions.showEnergyInfo() 416 | let energyInfo = _commonFunctions.getTodaysRuntimeStorage('energy') 417 | if (!_has_next) { 418 | showFloaty('本次共收取:' + (_post_energy - _pre_energy) + 'g 能量,累积共收取' + energyInfo.totalIncrease + 'g') 419 | } else { 420 | showCollectSummaryFloaty() 421 | } 422 | // 循环模式、或者有漏收 不返回home 423 | if ((!_config.is_cycle || !_has_next) && !_lost_someone) { 424 | automator.clickClose() 425 | sleep(1000) 426 | // 返回最小化支付宝 427 | _commonFunctions.minimize() 428 | } 429 | } 430 | 431 | /*********************** 432 | * 收取能量 433 | ***********************/ 434 | 435 | /** 436 | * 收集目标能量球能量 437 | * 438 | * @param {*} energy_ball 能量球对象 439 | * @param {boolean} isOwn 是否收集自身能量 440 | * @param {boolean} isDesc 是否是desc类型 441 | */ 442 | const collectBallEnergy = function (energy_ball, isOwn, isDesc) { 443 | if (_config.skip_five && !isOwn) { 444 | let regexCheck = /(\d+)克/ 445 | let execResult 446 | if (isDesc) { 447 | debugInfo('获取能量球desc数据') 448 | execResult = regexCheck.exec(energy_ball.desc()) 449 | } else { 450 | debugInfo('获取能量球text数据') 451 | execResult = regexCheck.exec(energy_ball.text()) 452 | } 453 | if (execResult.length > 1 && parseInt(execResult[1]) <= 5) { 454 | debugInfo( 455 | '能量小于等于五克跳过收取 ' + isDesc ? energy_ball.desc() : energy_ball.text() 456 | ) 457 | return 458 | } 459 | } 460 | debugInfo(isDesc ? energy_ball.desc() : energy_ball.text()) 461 | automator.clickCenter(energy_ball) 462 | if (!isOwn) { 463 | _collect_any = true 464 | } 465 | sleep(300) 466 | } 467 | 468 | // 收取能量 469 | const collectEnergy = function (own) { 470 | let isOwn = own || false 471 | let ballCheckContainer = _widgetUtils.widgetGetAll(_config.collectable_energy_ball_content, null, true) 472 | if (ballCheckContainer !== null) { 473 | debugInfo('能量球存在') 474 | ballCheckContainer.target 475 | .forEach(function (energy_ball) { 476 | collectBallEnergy(energy_ball, isOwn, ballCheckContainer.isDesc) 477 | }) 478 | } else { 479 | debugInfo('无能量球可收取') 480 | } 481 | } 482 | 483 | const findAndCollect = function () { 484 | let scanner = new FriendListScanner() 485 | scanner.init({ currentTime: _current_time, increaseEnergy: _post_energy - _pre_energy }) 486 | let executeResult = scanner.start() 487 | // 执行失败 返回 true 488 | if (executeResult === true) { 489 | _lost_someone = true 490 | } else { 491 | _lost_someone = executeResult.loatSomeone 492 | _collect_any = executeResult.collectAny 493 | _friends_min_countdown = executeResult.minCountdown 494 | } 495 | scanner.destory() 496 | return _lost_someone 497 | } 498 | 499 | 500 | /*********************** 501 | * 主要函数 502 | ***********************/ 503 | 504 | // 收取自己的能量 505 | const collectOwn = function () { 506 | _commonFunctions.addOpenPlacehold('开始收集自己能量') 507 | let restartCount = 0 508 | let waitFlag 509 | let startWait = 1000 510 | startApp() 511 | if (!_config.is_cycle) { 512 | // 首次启动等待久一点 513 | sleep(1500) 514 | } 515 | while (!(waitFlag = _widgetUtils.homePageWaiting()) && restartCount++ < 5) { 516 | warnInfo('程序未启动,尝试再次唤醒') 517 | automator.clickClose() 518 | debugInfo('关闭H5') 519 | if (restartCount >= 3) { 520 | startWait += 200 * restartCount 521 | home() 522 | } 523 | sleep(1000) 524 | // 解锁并启动 525 | unlocker.exec() 526 | startApp(false) 527 | sleep(startWait) 528 | } 529 | if (!waitFlag && restartCount >= 5) { 530 | logInfo('退出脚本') 531 | engines.myEngine().forceStop(); 532 | } 533 | logInfo('进入个人首页成功') 534 | clearPopup() 535 | getPreEnergy() 536 | debugInfo('准备收集自己能量') 537 | collectEnergy(true) 538 | if (!_config.is_cycle) { 539 | debugInfo('准备计算最短时间') 540 | getMinCountdownOwn() 541 | } 542 | _commonFunctions.addClosePlacehold("收集自己的能量完毕") 543 | _fisrt_running = false 544 | } 545 | 546 | // 收取好友的能量 547 | const collectFriend = function () { 548 | _commonFunctions.addOpenPlacehold('开始收集好友能量') 549 | automator.enterFriendList() 550 | let enterFlag = _widgetUtils.friendListWaiting() 551 | if (!enterFlag) { 552 | return false 553 | } 554 | if (true === findAndCollect()) { 555 | _min_countdown = 0 556 | _has_next = true 557 | _current_time = _current_time == 0 ? 0 : _current_time - 1 558 | errorInfo('收集好友能量失败,重新开始') 559 | _re_try++ 560 | return false 561 | } 562 | _commonFunctions.addClosePlacehold("收集好友能量结束") 563 | if (!_config.is_cycle && !_lost_someone) { 564 | getMinCountdown() 565 | } 566 | generateNext() 567 | getPostEnergy() 568 | } 569 | 570 | const Executor = function () { 571 | this.eventSettingThread = null 572 | this.stopListenThread = null 573 | this.needRestart = false 574 | this.setupEventListeners = function () { 575 | this.eventSettingThread = threads.start(function () { 576 | events.setMaxListeners(0) 577 | events.observeToast() 578 | }) 579 | } 580 | 581 | /** 582 | * 监听音量上键直接关闭 583 | */ 584 | this.listenStopCollect = function () { 585 | this.interruptStopListenThread() 586 | this.stopListenThread = threads.start(function () { 587 | infoLog('即将收取能量,运行中可按音量上键关闭', true) 588 | events.observeKey() 589 | events.onceKeyDown('volume_up', function (event) { 590 | if (_config.autoSetBrightness) { 591 | device.setBrightnessMode(1) 592 | } 593 | _runningQueueDispatcher.removeRunningTask() 594 | engines.myEngine().forceStop() 595 | exit() 596 | }) 597 | }) 598 | } 599 | 600 | this.readyForStart = function () { 601 | // 解锁其实在main里面已经执行 这里是为了容错 觉得没必要可以注释掉 602 | unlocker.exec() 603 | _runningQueueDispatcher.addRunningTask() 604 | if (_config.tryGetExactlyPackage) { 605 | _commonFunctions.showDialogAndWait(true) 606 | _commonFunctions.recordCurrentPackage() 607 | } else { 608 | _commonFunctions.recordCurrentPackage() 609 | _commonFunctions.showDialogAndWait(true) 610 | } 611 | this.listenStopCollect() 612 | _commonFunctions.showEnergyInfo() 613 | } 614 | 615 | this.endLoop = function () { 616 | this.interruptStopListenThread() 617 | events.removeAllListeners() 618 | _runningQueueDispatcher.removeRunningTask() 619 | if (_config.auto_lock === true && unlocker.needRelock() === true) { 620 | debugInfo('重新锁定屏幕') 621 | automator.lockScreen() 622 | } 623 | if (_config.autoSetBrightness) { 624 | device.setBrightnessMode(1) 625 | } 626 | } 627 | 628 | this.interruptStopListenThread = function () { 629 | if (this.stopListenThread !== null) { 630 | this.stopListenThread.interrupt() 631 | this.stopListenThread = null 632 | } 633 | } 634 | 635 | this.checkRestart = function () { 636 | logInfo('收取结束') 637 | if (this.eventSettingThread != null) { 638 | this.eventSettingThread.interrupt() 639 | this.eventSettingThread = null 640 | } 641 | if (this.needRestart) { 642 | // 设置三分钟后重试 643 | _commonFunctions.setUpAutoStart(3) 644 | _runningQueueDispatcher.removeRunningTask() 645 | exit() 646 | } else { 647 | setTimeout(() => { 648 | exit() 649 | }, 30000) 650 | } 651 | } 652 | } 653 | 654 | /** 655 | * 循环运行 656 | */ 657 | const CycleExecutor = function () { 658 | Executor.call(this) 659 | 660 | this.execute = function () { 661 | this.setupEventListeners() 662 | this.needRestart = false 663 | try { 664 | this.readyForStart() 665 | _current_time = 0 666 | while (true) { 667 | _current_time++ 668 | _commonFunctions.showEnergyInfo(_current_time) 669 | // 增加当天运行总次数 670 | _commonFunctions.increaseRunTimes() 671 | infoLog("========循环第" + _current_time + "次运行========") 672 | showCollectSummaryFloaty() 673 | try { 674 | collectOwn() 675 | if (collectFriend() === false) { 676 | // 收集失败,重新开始 677 | _lost_someone = true 678 | _current_time = _current_time == 0 ? 0 : _current_time - 1 679 | _has_next = true 680 | } 681 | } catch (e) { 682 | errorInfo('发生异常 [' + e + '] [' + e.message + ']') 683 | _current_time = _current_time == 0 ? 0 : _current_time - 1 684 | _lost_someone = true 685 | _has_next = true 686 | _re_try = 0 687 | } 688 | if (!_lost_someone && (_has_next === false || _re_try > 5)) { 689 | break 690 | } 691 | logInfo('========本轮结束========') 692 | } 693 | } catch (e) { 694 | errorInfo('发生异常,终止程序 [' + e + '] [' + e.message + ']') 695 | this.needRestart = true 696 | } 697 | this.endLoop() 698 | this.checkRestart() 699 | // 返回最小化支付宝 700 | _commonFunctions.minimize() 701 | } 702 | } 703 | 704 | /** 705 | * 计时运行 706 | */ 707 | const CountdownExecutor = function () { 708 | Executor.call(this) 709 | 710 | this.execute = function () { 711 | this.setupEventListeners() 712 | try { 713 | _current_time = 0 714 | while (true) { 715 | _collect_any = false 716 | if (_lost_someone) { 717 | warnInfo('上一次收取有漏收,再次收集', true) 718 | } else { 719 | if (_min_countdown > 0) { 720 | // 提前10秒左右结束计时 721 | let delayTime = 10 / 60.0 722 | // 延迟自动启动,用于防止autoJs自动崩溃等情况下导致的问题 723 | delayTime += (_config.delayStartTime || 5000) / 60000.0 724 | _commonFunctions.setUpAutoStart(_min_countdown - delayTime) 725 | _runningQueueDispatcher.removeRunningTask() 726 | // 如果不驻留悬浮窗 则不延迟,直接关闭 727 | if (_config.notLingeringFloatWindow) { 728 | exit() 729 | } else { 730 | _commonFunctions.commonDelay(_min_countdown - delayTime) 731 | _commonFunctions.checkCaptureScreenPermission() 732 | } 733 | } 734 | } 735 | this.readyForStart() 736 | let runTime = _commonFunctions.increaseRunTimes() 737 | infoLog("========第" + runTime + "次运行========") 738 | showCollectSummaryFloaty() 739 | debugInfo('展示悬浮窗完毕') 740 | _current_time++ 741 | try { 742 | collectOwn() 743 | if (collectFriend() === false) { 744 | // 收集失败,重新开始 745 | _lost_someone = true 746 | _current_time = _current_time == 0 ? 0 : _current_time - 1 747 | _min_countdown = 0 748 | _has_next = true 749 | } 750 | } catch (e) { 751 | errorInfo('发生异常 [' + e + '] [' + e.message + ']') 752 | _current_time = _current_time == 0 ? 0 : _current_time - 1 753 | _lost_someone = true 754 | _min_countdown = 0 755 | _has_next = true 756 | _re_try = 0 757 | } 758 | // 当前没有遗漏 准备结束当前循环 759 | if (!_lost_someone) { 760 | this.endLoop() 761 | if (_has_next === false || _re_try > 5) { 762 | break 763 | } 764 | } 765 | } 766 | logInfo('========本轮结束========') 767 | } catch (e) { 768 | errorInfo('发生异常,终止程序 [' + e + '] [' + e.message + ']') 769 | this.needRestart = true 770 | } 771 | this.checkRestart() 772 | } 773 | } 774 | 775 | return { 776 | exec: function () { 777 | let executor = null 778 | if (_config.is_cycle) { 779 | executor = new CycleExecutor() 780 | } else { 781 | executor = new CountdownExecutor() 782 | } 783 | executor.execute() 784 | } 785 | } 786 | } 787 | 788 | module.exports = new Ant_forest() 789 | -------------------------------------------------------------------------------- /core/ImgBasedFriendListScanner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-11-11 09:17:29 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-06 15:30:10 6 | * @Description: 7 | */ 8 | importClass(com.tony.BitCheck) 9 | let _widgetUtils = typeof WidgetUtils === 'undefined' ? require('../lib/WidgetUtils.js') : WidgetUtils 10 | let automator = require('../lib/Automator.js') 11 | let _commonFunctions = typeof commonFunctions === 'undefined' ? require('../lib/CommonFunction.js') : commonFunctions 12 | let _FloatyInstance = typeof FloatyInstance === 'undefined' ? require('../lib/FloatyUtil.js') : FloatyInstance 13 | let _config = typeof config === 'undefined' ? require('../config.js').config : config 14 | let FileUtils = require('../lib/FileUtils.js') 15 | 16 | let _avil_list = [] 17 | let _increased_energy = 0 18 | let _collect_any = false 19 | let _min_countdown = 10000 20 | let _current_time = 0 21 | 22 | const _package_name = 'com.eg.android.AlipayGphone' 23 | 24 | 25 | /** 26 | * 展示当前累积收集能量信息,累加已记录的和当前运行轮次所增加的 27 | * 28 | * @param {本次增加的能量值} increased 29 | */ 30 | const showCollectSummaryFloaty = function (increased) { 31 | increased = increased || 0 32 | _increased_energy += increased 33 | if (_config.is_cycle) { 34 | _commonFunctions.showCollectSummaryFloaty0(_increased_energy, _current_time, increased) 35 | } else { 36 | _commonFunctions.showCollectSummaryFloaty0(null, null, _increased_energy) 37 | } 38 | } 39 | 40 | /** 41 | * 收集目标能量球能量 42 | * 43 | * @param {*} energy_ball 能量球对象 44 | * @param {boolean} isDesc 是否是desc类型 45 | */ 46 | const collectBallEnergy = function (energy_ball, isDesc) { 47 | if (_config.skip_five && !isOwn) { 48 | let regexCheck = /(\d+)克/ 49 | let execResult 50 | if (isDesc) { 51 | debugInfo('获取能量球desc数据') 52 | execResult = regexCheck.exec(energy_ball.desc()) 53 | } else { 54 | debugInfo('获取能量球text数据') 55 | execResult = regexCheck.exec(energy_ball.text()) 56 | } 57 | if (execResult.length > 1 && parseInt(execResult[1]) <= 5) { 58 | debugInfo( 59 | '能量小于等于五克跳过收取 ' + isDesc ? energy_ball.desc() : energy_ball.text() 60 | ) 61 | return 62 | } 63 | } 64 | debugInfo(isDesc ? energy_ball.desc() : energy_ball.text()) 65 | automator.clickCenter(energy_ball) 66 | _collect_any = true 67 | sleep(300) 68 | } 69 | 70 | // 收取能量 71 | const collectEnergy = function (own) { 72 | let isOwn = own || false 73 | let ballCheckContainer = _widgetUtils.widgetGetAll(_config.collectable_energy_ball_content, null, true) 74 | if (ballCheckContainer !== null) { 75 | debugInfo('能量球存在') 76 | ballCheckContainer.target 77 | .forEach(function (energy_ball) { 78 | collectBallEnergy(energy_ball, isOwn, ballCheckContainer.isDesc) 79 | }) 80 | } else { 81 | debugInfo('无能量球可收取') 82 | } 83 | } 84 | 85 | // 收取能量同时帮好友收取 86 | const collectAndHelp = function (needHelp) { 87 | // 收取好友能量 88 | collectEnergy() 89 | let screen = null 90 | _commonFunctions.waitFor(function () { 91 | screen = captureScreen() 92 | }, 500) 93 | if (!screen) { 94 | warnInfo('获取截图失败,无法帮助收取能量') 95 | return 96 | } 97 | // 帮助好友收取能量 98 | let energyBalls 99 | if ( 100 | className('Button').descMatches(/\s/).exists() 101 | ) { 102 | energyBalls = className('Button').descMatches(/\s/).untilFind() 103 | } else if ( 104 | className('Button').textMatches(/\s/).exists() 105 | ) { 106 | energyBalls = className('Button').textMatches(/\s/).untilFind() 107 | } 108 | if (energyBalls && energyBalls.length > 0) { 109 | let length = energyBalls.length 110 | let helped = false 111 | let colors = _config.helpBallColors || ['#f99236', '#f7af70'] 112 | energyBalls.forEach(function (energy_ball) { 113 | let bounds = energy_ball.bounds() 114 | let o_x = bounds.left, 115 | o_y = bounds.top, 116 | o_w = bounds.width() + 20, 117 | o_h = bounds.height() + 20, 118 | threshold = _config.color_offset 119 | for (let color of colors) 120 | if ( 121 | images.findColor(screen, color, { 122 | region: [o_x, o_y, o_w, o_h], 123 | threshold: threshold 124 | }) 125 | ) { 126 | automator.clickCenter(energy_ball) 127 | helped = true 128 | _collect_any = true 129 | sleep(200) 130 | debugInfo("找到帮收取能量球颜色匹配" + color) 131 | break 132 | } 133 | }) 134 | if (!helped && needHelp) { 135 | warnInfo(['未能找到帮收能量球需要增加匹配颜色组 当前{}', colors]) 136 | } 137 | // 当数量大于等于6且帮助收取后,重新进入 138 | if (helped && length >= 6) { 139 | return true 140 | } 141 | } 142 | } 143 | 144 | // 判断并记录保护罩 145 | const recordProtected = function (toast) { 146 | if (toast.indexOf('能量罩') > 0) { 147 | recordCurrentProtected() 148 | } 149 | } 150 | 151 | const recordCurrentProtected = function () { 152 | let title = textContains('的蚂蚁森林') 153 | .findOne(_config.timeout_findOne) 154 | .text() 155 | _commonFunctions.addNameToProtect(title.substring(0, title.indexOf('的'))) 156 | } 157 | 158 | // 检测能量罩 159 | const protectDetect = function (filter) { 160 | filter = typeof filter == null ? '' : filter 161 | // 在新线程中开启监听 162 | return threads.start(function () { 163 | events.onToast(function (toast) { 164 | if (toast.getPackageName().indexOf(filter) >= 0) 165 | recordProtected(toast.getText()) 166 | }) 167 | }) 168 | } 169 | 170 | const protectInfoDetect = function () { 171 | let usingInfo = _widgetUtils.widgetGetOne(_config.using_protect_content, 50, true, true) 172 | if (usingInfo !== null) { 173 | let target = usingInfo.target 174 | debugInfo(['found using protect info, bounds:{}', target.bounds()], true) 175 | let parent = target.parent().parent() 176 | let targetRow = parent.row() 177 | let time = parent.child(1).text() 178 | if (!time) { 179 | time = parent.child(1).desc() 180 | } 181 | let isToday = true 182 | let yesterday = _widgetUtils.widgetGetOne('昨天', 50, true, true) 183 | let yesterdayRow = null 184 | if (yesterday !== null) { 185 | yesterdayRow = yesterday.target.row() 186 | // warnInfo(yesterday.target.indexInParent(), true) 187 | isToday = yesterdayRow > targetRow 188 | } 189 | if (!isToday) { 190 | // 获取前天的日期 191 | let dateBeforeYesterday = formatDate(new Date(new Date().getTime() - 3600 * 24 * 1000 * 2), 'MM-dd') 192 | let dayBeforeYesterday = _widgetUtils.widgetGetOne(dateBeforeYesterday, 50, true, true) 193 | if (dayBeforeYesterday !== null) { 194 | let dayBeforeYesterdayRow = dayBeforeYesterday.target.row() 195 | if (dayBeforeYesterdayRow < targetRow) { 196 | debugInfo('能量罩使用时间已超时,前天之前的数据') 197 | return false 198 | } else { 199 | debugInfo(['前天row:{}', dayBeforeYesterdayRow]) 200 | } 201 | } 202 | } 203 | debugInfo(['using time:{}-{} rows: yesterday[{}] target[{}]', (isToday ? '今天' : '昨天'), time, yesterdayRow, targetRow], true) 204 | recordCurrentProtected() 205 | return true 206 | } else { 207 | debugInfo('not found using protect info') 208 | } 209 | return false 210 | } 211 | 212 | const collectTargetFriend = function (obj) { 213 | let rentery = false 214 | if (!obj.protect) { 215 | let temp = protectDetect(_package_name) 216 | //automator.click(obj.target.centerX(), obj.target.centerY()) 217 | debugInfo('等待进入好友主页:' + obj.name) 218 | let restartLoop = false 219 | let count = 1 220 | automator.click(obj.point.x, obj.point.y) 221 | ///sleep(1000) 222 | while (!_widgetUtils.friendHomeWaiting()) { 223 | debugInfo( 224 | '未能进入主页,尝试再次进入 count:' + count++ 225 | ) 226 | automator.click(obj.point.x, obj.point.y) 227 | sleep(1000) 228 | if (count > 5) { 229 | warnInfo('重试超过5次,取消操作') 230 | restartLoop = true 231 | break 232 | } 233 | } 234 | if (restartLoop) { 235 | errorInfo('页面流程出错,重新开始') 236 | return false 237 | } 238 | if (protectInfoDetect()) { 239 | warnInfo(['{} 好友已使用能量保护罩,跳过收取', obj.name]) 240 | automator.back() 241 | return 242 | } 243 | debugInfo('准备开始收取') 244 | 245 | let preGot 246 | let preE 247 | try { 248 | preGot = _widgetUtils.getYouCollectEnergy() || 0 249 | preE = _widgetUtils.getFriendEnergy() 250 | } catch (e) { errorInfo("[" + obj.name + "]获取收集前能量异常" + e) } 251 | if (_config.help_friend) { 252 | rentery = collectAndHelp(obj.isHelp) 253 | } else { 254 | collectEnergy() 255 | } 256 | try { 257 | let postGet = _widgetUtils.getYouCollectEnergy() || 0 258 | let postE = _widgetUtils.getFriendEnergy() 259 | if (!obj.isHelp && postGet !== null && preGot !== null) { 260 | let gotEnergy = postGet - preGot 261 | debugInfo("开始收集前:" + preGot + "收集后:" + postGet) 262 | if (gotEnergy) { 263 | let needWaterback = _commonFunctions.recordFriendCollectInfo({ 264 | friendName: obj.name, 265 | friendEnergy: postE, 266 | postCollect: postGet, 267 | preCollect: preGot, 268 | helpCollect: 0 269 | }) 270 | try { 271 | if (needWaterback) { 272 | _widgetUtils.wateringFriends() 273 | gotEnergy -= 10 274 | } 275 | } catch (e) { 276 | errorInfo('收取[' + obj.name + ']' + gotEnergy + 'g 大于阈值:' + _config.wateringThreshold + ' 回馈浇水失败 ' + e) 277 | } 278 | logInfo([ 279 | "收取好友:{} 能量 {}g {}", 280 | obj.name, gotEnergy, (needWaterback ? '其中浇水10g' : '') 281 | ]) 282 | showCollectSummaryFloaty(gotEnergy) 283 | } else { 284 | debugInfo("收取好友:" + obj.name + " 能量 " + gotEnergy + "g") 285 | 286 | } 287 | } else if (obj.isHelp && postE !== null && preE !== null) { 288 | let gotEnergy = postE - preE 289 | debugInfo("开始帮助前:" + preE + " 帮助后:" + postE) 290 | if (gotEnergy) { 291 | logInfo("帮助好友:" + obj.name + " 回收能量 " + gotEnergy + "g") 292 | _commonFunctions.recordFriendCollectInfo({ 293 | friendName: obj.name, 294 | friendEnergy: postE, 295 | postCollect: postGet, 296 | preCollect: preGot, 297 | helpCollect: gotEnergy 298 | }) 299 | } else { 300 | debugInfo("帮助好友:" + obj.name + " 回收能量 " + gotEnergy + "g") 301 | } 302 | } 303 | } catch (e) { 304 | errorInfo("[" + obj.name + "]获取收取后能量异常" + e) 305 | } 306 | automator.back() 307 | temp.interrupt() 308 | debugInfo('好友能量收取完毕, 回到好友排行榜') 309 | let returnCount = 0 310 | while (!_widgetUtils.friendListWaiting()) { 311 | sleep(1000) 312 | if (returnCount++ === 2) { 313 | // 等待两秒后再次触发 314 | automator.back() 315 | } 316 | if (returnCount > 5) { 317 | errorInfo('返回好友排行榜失败,重新开始') 318 | return false 319 | } 320 | } 321 | if (rentery) { 322 | obj.isHelp = false 323 | return collectTargetFriend(obj) 324 | } 325 | } 326 | return true 327 | } 328 | 329 | // 根据可收取列表收取好友 330 | const collectAvailableList = function () { 331 | while (_avil_list.length) { 332 | if (false === collectTargetFriend(_avil_list.shift())) { 333 | warnInfo('收取目标好友失败,向上抛出') 334 | return false 335 | } 336 | } 337 | } 338 | 339 | 340 | /** 341 | * 记录好友信息 342 | * @param {Object} screen 343 | */ 344 | const checkAvailiable = function (point, isHelp) { 345 | automator.click(point.x, point.y) 346 | let temp = {} 347 | // TODO 获取好友名称 348 | let container = '' 349 | // 记录好友ID 350 | temp.name = container.name 351 | // 记录是否有保护罩 352 | temp.protect = _commonFunctions.checkIsProtected(temp.name) 353 | // 记录是否是帮助收取 354 | temp.isHelp = isHelp 355 | // 不在白名单的 添加到可收取列表 356 | if (_config.white_list.indexOf(temp.name) < 0) { 357 | _avil_list.push(temp) 358 | } 359 | } 360 | 361 | function BFSColorCenterCalculator (img, color, point, threshold) { 362 | this.img = img 363 | this.color = color 364 | this.point = point 365 | this.threshold = threshold 366 | 367 | this.nearbyNodes = new this.Queue() 368 | 369 | 370 | this.init = function () { 371 | 372 | } 373 | 374 | 375 | 376 | 377 | this.Queue = function () { 378 | this.nodes = [] 379 | this.size = 0 380 | this.idxEnd = 0 381 | this.idxStart = 0 382 | 383 | this.dequeue = function () { 384 | if (this.size > 0) { 385 | return this.nodes[this.idxStart++] 386 | } 387 | } 388 | 389 | this.push = function (item) { 390 | this.size++ 391 | this.nodes.push(item) 392 | this.idxEnd++ 393 | } 394 | 395 | this.isEmpty = function () { 396 | return this.size === 0 397 | } 398 | } 399 | } 400 | 401 | const Stack = function () { 402 | this.size = 0 403 | this.innerArray = [] 404 | this.index = -1 405 | 406 | this.isEmpty = function () { 407 | return this.size === 0 408 | } 409 | 410 | this.push = function (val) { 411 | this.innerArray.push(val) 412 | this.index++ 413 | this.size++ 414 | } 415 | 416 | this.peek = function () { 417 | if (this.isEmpty()) { 418 | return null 419 | } 420 | return this.innerArray[this.index] 421 | } 422 | 423 | this.pop = function (val) { 424 | if (this.isEmpty()) { 425 | return null 426 | } 427 | this.size-- 428 | return this.innerArray.splice(this.index--)[0] 429 | } 430 | 431 | this.print = function () { 432 | if (this.isEmpty()) { 433 | return 434 | } 435 | this.innerArray.forEach(val => { 436 | console.log(val) 437 | }) 438 | } 439 | } 440 | 441 | function CheckBit (maxVal) { 442 | this.BUFFER_LENGTH = Math.ceil(maxVal / 8) 443 | this.BYTE_SIZE = 1 << 3 444 | this.bytes = [] 445 | this.init() 446 | } 447 | 448 | CheckBit.prototype.init = function () { 449 | this.bytes = Array(this.BUFFER_LENGTH + 1).join(0).split('') 450 | } 451 | 452 | CheckBit.prototype.setBit = function (val) { 453 | let idx = ~~(val / this.BYTE_SIZE) 454 | let posi = 1 << (val % this.BYTE_SIZE) 455 | let unset = (this.bytes[idx] & posi) !== posi 456 | this.bytes[idx] = this.bytes[idx] | posi 457 | return unset 458 | } 459 | 460 | CheckBit.prototype.isUnchecked = function (point) { 461 | if (point.x < 880) { 462 | return false 463 | } 464 | // 1080 - 200 = 880 465 | return this.setBit((point.x - 880) * 10000 + point.y) 466 | } 467 | 468 | 469 | const BIT_MAX_VAL = 200 * 10000 + 2160 470 | // 计算中心点 471 | function ColorRegionCenterCalculator (img, color, point, threshold) { 472 | // 列表方式判断是否已校验 473 | this.checkedPoint = [] 474 | // 对象hash方式 475 | this.checkedHash = {} 476 | // JavaScript位运算方式 477 | this.checkBit = new CheckBit(BIT_MAX_VAL) 478 | // Java打包的位运算方式 479 | this.bitChecker = new BitCheck(BIT_MAX_VAL) 480 | this.checkedX = [] 481 | this.checkedY = [] 482 | this.img = img 483 | this.color = color 484 | this.point = point 485 | this.threshold = threshold 486 | 487 | /** 488 | * 获取所有同色区域的点集合 489 | */ 490 | this.getAllColorRegionPoints = function () { 491 | // this.checkedPoint.push(this.point) 492 | // this.checkedX = [this.point.x] 493 | // this.checkedY = [this.point.y] 494 | // let nearlyColorPoints = this.getNearly(point) 495 | log('初始点:' + JSON.stringify(this.point)) 496 | let nearlyColorPoints = this.getNearlyNorecursion(this.point) 497 | nearlyColorPoints = nearlyColorPoints || [] 498 | // nearlyColorPoints.push(point) 499 | // log('颜色检测完毕同色像素点总数:' + nearlyColorPoints.length + '校验像素点总数:' + this.checkedPoint.length ) 500 | // sleep(5000) 501 | // log('同色像素点总数:' + nearlyColorPoints.length + '\n集合:' + JSON.stringify(nearlyColorPoints)) 502 | // log('校验像素点总数:' + this.checkedPoint.length + '\n集合' + JSON.stringify(this.checkedPoint)) 503 | return nearlyColorPoints 504 | } 505 | 506 | /** 507 | * 获取颜色中心点 508 | */ 509 | this.getColorRegionCenter = function () { 510 | let maxX = -1 511 | let minX = 1080 + 10 512 | let maxY = -1 513 | let minY = 20000 514 | 515 | let nearlyColorPoints = this.getAllColorRegionPoints() 516 | if (nearlyColorPoints && nearlyColorPoints.length > 0) { 517 | console.log('同色点总数:' + nearlyColorPoints.length) 518 | let start = new Date().getTime() 519 | nearlyColorPoints.forEach((item, idx) => { 520 | if (maxX < item.x) { 521 | maxX = item.x 522 | } 523 | if (minX > item.x) { 524 | minX = item.x 525 | } 526 | if (maxY < item.y) { 527 | maxY = item.y 528 | } 529 | if (minY > item.y) { 530 | minY = item.y 531 | } 532 | }) 533 | log('计算中心点耗时' + (new Date().getTime() - start) + 'ms') 534 | let center = { 535 | x: parseInt((maxX + minX) / 2), 536 | y: parseInt((maxY + minY) / 2), 537 | same: nearlyColorPoints.length 538 | } 539 | // log('获取中心点位置为:' + JSON.stringify(center)) 540 | return center 541 | } else { 542 | console.log('没有找到同色点 原始位置:' + JSON.stringify(this.point)) 543 | return this.point 544 | } 545 | } 546 | 547 | 548 | this.isOutofScreen = function (point) { 549 | let width = 1080 550 | let height = 2160 551 | if (point.x >= width || point.x < 0 || point.y < 0 || point.y >= height) { 552 | return true 553 | } 554 | return false 555 | } 556 | 557 | this.getNearlyNorecursion = function (point) { 558 | let directs = [ 559 | [0, -1], 560 | [0, 1], 561 | [1, 0], 562 | [-1, 0] 563 | ] 564 | let stack = new Stack() 565 | stack.push(point) 566 | let nearlyPoints = [point] 567 | this.checkedPoint.push(point) 568 | // this.isUnchecked(point) 569 | // this.isUncheckedHash(point) 570 | // this.isUncheckedBit(point) 571 | this.isUncheckedBitJava(point) 572 | let step = 0 573 | let totalStart = new Date().getTime() 574 | // let totalCheckAndCreate = 0 575 | // let totalCheckColor = 0 576 | // let timestamp = 0 577 | while (!stack.isEmpty()) { 578 | let target = stack.peek() 579 | let allChecked = true 580 | for (let i = 0; i < 4; i++) { 581 | let direction = directs[i] 582 | // timestamp = new Date().getTime() 583 | let checkItem = this.getDirectionPoint(target, direction) 584 | // totalCheckAndCreate += new Date().getTime() - timestamp 585 | if (!checkItem) { 586 | continue 587 | } 588 | step++ 589 | allChecked = false 590 | // timestamp = new Date().getTime() 591 | if (images.detectsColor(this.img, this.color, checkItem.x, checkItem.y, this.threshold)) { 592 | nearlyPoints.push(checkItem) 593 | stack.push(checkItem) 594 | } 595 | // totalCheckColor += new Date().getTime() - timestamp 596 | } 597 | if (allChecked) { 598 | stack.pop() 599 | } 600 | } 601 | log('找了多个点 总计步数:' + step + '\n总耗时:' + (new Date().getTime() - totalStart) + 'ms') 602 | // log('判断是否校验耗时:' + totalCheckAndCreate + 'ms') 603 | // log('判断颜色耗时:' + totalCheckColor + 'ms') 604 | return nearlyPoints 605 | } 606 | 607 | this.isUncheckedBitJava = function (point) { 608 | if (point.x < 880) { 609 | return false 610 | } 611 | // 1080 - 200 = 880 612 | return this.bitChecker.isUnchecked((point.x - 880) * 10000 + point.y) 613 | } 614 | 615 | this.isUncheckedBit = function (point) { 616 | return this.checkBit.isUnchecked(point) 617 | } 618 | 619 | 620 | this.isUncheckedHash = function (point) { 621 | // let start = new Date().getTime() 622 | let key = point.x + '-' + point.y 623 | if (this.checkedHash[key]) { 624 | return false 625 | } 626 | this.checkedHash[key] = true 627 | return true 628 | } 629 | 630 | this.isUnchecked = function (point) { 631 | // 超出屏幕的直接返回false 632 | if (this.isOutofScreen(point)) { 633 | return false 634 | } 635 | let filted = this.checkedPoint.filter(checked => checked.x === point.x && checked.y === point.y) 636 | let checked = filted && filted.length > 0 637 | // sleep(5) 638 | if (!checked) { 639 | this.checkedPoint.push(point) 640 | } 641 | return !checked 642 | } 643 | 644 | this.getDirectionPoint = function (point, direct) { 645 | // log('准备获取附近节点:' + JSON.stringify(point)) 646 | let nearPoint = { 647 | x: point.x + direct[0], 648 | y: point.y + direct[1] 649 | } 650 | if (this.isOutofScreen(nearPoint) || !this.isUncheckedBitJava(nearPoint)) { 651 | // if (this.isOutofScreen(nearPoint) || !this.isUncheckedBit(nearPoint)) { 652 | // if (this.isOutofScreen(nearPoint) || !this.isUncheckedHash(nearPoint)) { 653 | // if (this.isOutofScreen(nearPoint) || !this.isUnchecked(nearPoint)) { 654 | return null 655 | } 656 | return nearPoint 657 | } 658 | 659 | } 660 | 661 | function ImgBasedFriendListScanner () { 662 | 663 | this.start = function () { 664 | _increased_energy = 0 665 | _min_countdown = 10000 666 | return this.collecting() 667 | } 668 | 669 | this.sortAndReduce = function (points, gap) { 670 | gap = gap || 110 671 | // 默认情况下已经排序了 没必要再次排序 672 | let last = -gap - 1 673 | let resultPoints = [] 674 | if (points && points.length > 0) { 675 | points.forEach(point => { 676 | if (point.y - last > gap) { 677 | resultPoints.push(point) 678 | last = point.y 679 | } else { 680 | // 距离过近的丢弃 681 | log('丢弃距离较上一个比较近的:' + JSON.stringify(point)) 682 | } 683 | }) 684 | log('重新分析后的点:' + JSON.stringify(resultPoints)) 685 | } 686 | return resultPoints 687 | } 688 | 689 | this.collecting = function () { 690 | let screen = _commonFunctions.checkCaptureScreenPermission() 691 | console.show() 692 | console.log('获取到screen' + (screen === null ? '失败' : '成功')) 693 | _FloatyInstance.setFloatyTextColor('#FF0000') 694 | let helpPoints = this.sortAndReduce(this.detectHelp(screen)) 695 | if (helpPoints && helpPoints.length > 0) { 696 | helpPoints.forEach(point => { 697 | let calculator = new ColorRegionCenterCalculator(screen, _config.can_help_color || '#f99236', point, _config.color_offset) 698 | point = calculator.getColorRegionCenter() 699 | console.log('设置悬浮窗位置:' + JSON.stringify(point)) 700 | _FloatyInstance.setFloatyInfo(point, '\'可帮助收取') 701 | sleep(5000) 702 | }) 703 | } 704 | let collectPoints = this.sortAndReduce(this.detectCollect(screen)) 705 | if (collectPoints && collectPoints.length > 0) { 706 | collectPoints.forEach(point => { 707 | let calculator = new ColorRegionCenterCalculator(screen, _config.can_collect_color || '#1da06a', point, _config.color_offset) 708 | point = calculator.getColorRegionCenter() 709 | console.log('设置悬浮窗位置:' + JSON.stringify(point)) 710 | _FloatyInstance.setFloatyInfo(point, '\'可能可收取') 711 | sleep(5000) 712 | }) 713 | } 714 | } 715 | 716 | this.detectHelp = function (img) { 717 | let helpPoints = this.detectColors(img, _config.can_help_color || '#f99236') 718 | console.log('可帮助的点:' + JSON.stringify(helpPoints)) 719 | return helpPoints 720 | } 721 | 722 | this.detectCollect = function (img) { 723 | let collectPoints = this.detectColors(img, _config.can_collect_color || '#1da06a') 724 | console.log('可收取的点:' + JSON.stringify(collectPoints)) 725 | return collectPoints 726 | } 727 | 728 | this.detectColors = function (img, color) { 729 | log('准备检测颜色:' + color) 730 | let endY = 2160 - 200 731 | let runningY = 100 732 | let startX = 1080 - 100 733 | let regionWindow = [startX, runningY, 100, 200] 734 | let findColorPoints = [] 735 | let countdown = new Countdown() 736 | while (runningY < endY) { 737 | log('检测区域:' + JSON.stringify(regionWindow)) 738 | let point = images.findColor(img, color, { 739 | region: regionWindow, 740 | threshold: _config.color_offset || 20 741 | }) 742 | countdown.summary('检测初始点') 743 | if (point) { 744 | findColorPoints.push(point) 745 | } 746 | runningY += 200 747 | if (runningY > endY) { 748 | runningY = endY 749 | } 750 | regionWindow = [startX, runningY, 100, 200] 751 | countdown.restart() 752 | } 753 | return findColorPoints 754 | } 755 | } 756 | 757 | function Countdown () { 758 | this.start = new Date().getTime() 759 | this.getCost = function () { 760 | return new Date().getTime() - this.start 761 | } 762 | 763 | this.summary = function (content) { 764 | console.log(content + '耗时' + this.getCost() + 'ms') 765 | } 766 | 767 | this.restart = function () { 768 | this.start = new Date().getTime() 769 | } 770 | 771 | } 772 | 773 | module.exports = ImgBasedFriendListScanner -------------------------------------------------------------------------------- /lib/Automator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: NickHopps 3 | * @Last Modified by: TonyJiangWJ 4 | * @Last Modified time: 2019-12-04 10:24:21 5 | * @Description: 自动化模块(多版本支持) 6 | */ 7 | let _config = typeof config === 'undefined' ? require('../config.js').config : config 8 | 9 | const hasRootPermission = function () { 10 | return files.exists("/sbin/su") || files.exists("/system/xbin/su") || files.exists("/system/bin/su") 11 | } 12 | 13 | function CommonAutomation () { 14 | 15 | this.scrollDown = function (speed) { 16 | let millis = speed || _config.scrollDownSpeed || 500 17 | let deviceHeight = device.height || 1900 18 | let bottomHeight = _config.bottomHeight || 250 19 | this.swipe(400, deviceHeight - bottomHeight, 600, 200, millis) 20 | } 21 | 22 | this.scrollUpAndDown = function (speed) { 23 | let millis = speed || _config.scrollDownSpeed || 500 24 | millis /= 2 25 | let deviceHeight = device.height || 1900 26 | let bottomHeight = _config.bottomHeight || 250 27 | // 下拉 28 | this.swipe(400, deviceHeight / 3, 600, deviceHeight / 3 * 2, millis) 29 | sleep(millis + 20) 30 | this.swipe(400, deviceHeight - bottomHeight, 600, 200, millis) 31 | } 32 | 33 | this.clickBack = function () { 34 | let hasButton = false 35 | if (descEndsWith('返回').exists()) { 36 | descEndsWith('返回') 37 | .findOne(_config.timeout_findOne) 38 | .click() 39 | hasButton = true 40 | } else if (textEndsWith('返回').exists()) { 41 | textEndsWith('返回') 42 | .findOne(_config.timeout_findOne) 43 | .click() 44 | hasButton = true 45 | } 46 | if (hasButton) { 47 | sleep(200) 48 | } 49 | return hasButton 50 | } 51 | 52 | this.clickClose = function () { 53 | let hasButton = false 54 | if (descEndsWith('关闭').exists()) { 55 | descEndsWith('关闭') 56 | .findOne(_config.timeout_findOne) 57 | .click() 58 | hasButton = true 59 | } else if (textEndsWith('关闭').exists()) { 60 | textEndsWith('关闭') 61 | .findOne(_config.timeout_findOne) 62 | .click() 63 | hasButton = true 64 | } 65 | if (hasButton) { 66 | sleep(200) 67 | } 68 | return hasButton 69 | } 70 | 71 | this.enterFriendList = function () { 72 | if (descEndsWith(_config.enter_friend_list_ui_content).exists()) { 73 | descEndsWith(_config.enter_friend_list_ui_content) 74 | .findOne(_config.timeout_findOne) 75 | .click() 76 | } else if (textEndsWith(_config.enter_friend_list_ui_content).exists()) { 77 | textEndsWith(_config.enter_friend_list_ui_content) 78 | .findOne(_config.timeout_findOne) 79 | .click() 80 | } 81 | sleep(200) 82 | } 83 | } 84 | function Automation_root () { 85 | CommonAutomation.call(this) 86 | 87 | this.check_root = function () { 88 | if (!(files.exists("/sbin/su") || files.exists("/system/xbin/su") || files.exists("/system/bin/su"))) throw new Error("未获取ROOT权限"); 89 | } 90 | 91 | this.click = function (x, y) { 92 | this.check_root(); 93 | return (shell("input tap " + x + " " + y, true).code === 0); 94 | } 95 | 96 | this.swipe = function (x1, y1, x2, y2, duration) { 97 | this.check_root(); 98 | return (shell("input swipe " + x1 + " " + y1 + " " + x2 + " " + y2 + " " + duration, true).code === 0); 99 | } 100 | 101 | this.gesture = function (duration, points) { 102 | this.check_root(); 103 | let len = points.length, 104 | step = duration / len, 105 | start = points.shift(); 106 | 107 | // 使用 RootAutomator 模拟手势,仅适用于安卓5.0及以上 108 | let ra = new RootAutomator(); 109 | ra.touchDown(start[0], start[1]); 110 | sleep(step); 111 | points.forEach(function (el) { 112 | ra.touchMove(el[0], el[1]); 113 | sleep(step); 114 | }); 115 | ra.touchUp(); 116 | ra.exit(); 117 | return true; 118 | } 119 | 120 | this.back = function () { 121 | this.check_root(); 122 | return (shell("input keyevent KEYCODE_BACK", true).code === 0); 123 | } 124 | 125 | this.lockScreen = function () { 126 | return (shell("input keyevent 26", true).code === 0) 127 | } 128 | 129 | } 130 | 131 | function Automation () { 132 | CommonAutomation.call(this) 133 | 134 | this.click = function (x, y) { 135 | return click(x, y); 136 | } 137 | 138 | this.swipe = function (x1, y1, x2, y2, duration) { 139 | return swipe(x1, y1, x2, y2, duration); 140 | } 141 | 142 | this.gesture = function (duration, points) { 143 | return gesture(duration, points); 144 | } 145 | 146 | this.back = function () { 147 | return back(); 148 | } 149 | 150 | /** 151 | * 下拉状态栏,点击锁屏按钮 152 | */ 153 | this.lockScreen = function () { 154 | swipe(500, 10, 500, 1000, 500) 155 | swipe(500, 10, 500, 1000, 500) 156 | // 点击锁屏按钮 157 | click(parseInt(_config.lock_x), parseInt(_config.lock_y)) 158 | } 159 | 160 | } 161 | 162 | const _automator = (device.sdkInt < 24 || hasRootPermission()) ? new Automation_root() : new Automation(); 163 | module.exports = { 164 | click: function (x, y) { 165 | return _automator.click(x, y); 166 | }, 167 | clickCenter: function (obj) { 168 | return _automator.click(obj.bounds().centerX(), obj.bounds().centerY()); 169 | }, 170 | swipe: function (x1, y1, x2, y2, duration) { 171 | return _automator.swipe(x1, y1, x2, y2, duration); 172 | }, 173 | gesture: function (duration, points) { 174 | return _automator.gesture(duration, points); 175 | }, 176 | back: function () { 177 | return _automator.back(); 178 | }, 179 | lockScreen: function () { 180 | return _automator.lockScreen(); 181 | }, 182 | scrollDown: function (speed) { 183 | if (_config.useCustomScrollDown) { 184 | return _automator.scrollDown(speed) 185 | } else { 186 | return scrollDown() 187 | } 188 | }, 189 | scrollUpAndDown: function (speed) { 190 | if (_config.useCustomScrollDown) { 191 | return _automator.scrollUpAndDown(speed) 192 | } else { 193 | let deviceHeight = device.height || 1900 194 | // 下拉 195 | this.swipe(400, deviceHeight / 3, 600, deviceHeight / 3 * 2, 150) 196 | sleep(200) 197 | scrollDown() 198 | } 199 | }, 200 | clickBack: function () { 201 | return _automator.clickBack() 202 | }, 203 | clickClose: function () { 204 | return _automator.clickClose() 205 | }, 206 | enterFriendList: function () { 207 | return _automator.enterFriendList() 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /lib/DateCompare.js: -------------------------------------------------------------------------------- 1 | const undefinedToZero = function (fieldValue) { 2 | return typeof fieldValue === 'undefined' ? 0 : fieldValue 3 | } 4 | 5 | function DateCompare(date) { 6 | 7 | this.year = undefinedToZero(date.getUTCFullYear()) 8 | this.month = undefinedToZero(date.getUTCMonth()) 9 | this.date = undefinedToZero(date.getUTCDate()) 10 | this.hours = undefinedToZero(date.getUTCHours()) 11 | this.minutes = undefinedToZero(date.getUTCMinutes()) 12 | this.seconds = undefinedToZero(date.getUTCSeconds()) 13 | this.milliseconds = undefinedToZero(date.getUTCMilliseconds()) 14 | this.originDate = date 15 | 16 | 17 | this.getCompareDateField = function () { 18 | return this.year * 10000 + this.month * 100 + this.date 19 | } 20 | 21 | this.getCompareTimeField = function () { 22 | return (this.hours * 3600 + this.minutes * 60 + this.seconds) * 1000 + this.milliseconds 23 | } 24 | 25 | /** 26 | * 自定义比较,比较各个字段 27 | */ 28 | this.customCompareTo = function (date) { 29 | // 先比较日期 30 | if (this.getCompareDateField() > targetDate.getCompareDateField()) { 31 | return 1 32 | } else if (this.getCompareDateField() < targetDate.getCompareDateField()) { 33 | return -1 34 | } 35 | // 日期相等,比较时间 36 | if (this.getCompareTimeField() > targetDate.getCompareTimeField()) { 37 | return 1 38 | } else if (this.getCompareTimeField() < targetDate.getCompareTimeField()) { 39 | return -1 40 | } else { 41 | return 0 42 | } 43 | } 44 | 45 | this.compareTo = function (date) { 46 | if (this.getTime() > date.getTime()) { 47 | return 1 48 | } else if (this.getTime() < date.getTime()) { 49 | return -1 50 | } else { 51 | return 0 52 | } 53 | } 54 | 55 | this.getTime = function () { 56 | return this.originDate.getTime() 57 | } 58 | } 59 | 60 | module.exports = DateCompare -------------------------------------------------------------------------------- /lib/DateUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-08-05 14:36:13 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-03 20:22:55 6 | * @Description: 7 | */ 8 | function DateUtil() { 9 | return { 10 | formatDate: function (date, fmt) { 11 | if (typeof fmt === 'undefined') { 12 | fmt = "yyyy-MM-dd HH:mm:ss" 13 | } 14 | 15 | var o = { 16 | 'M+': date.getMonth() + 1, // 月份 17 | 'd+': date.getDate(), // 日 18 | 'h+': date.getHours() % 12 === 0 ? 12 : date.getHours() % 12, // 小时 19 | 'H+': date.getHours(), // 小时 20 | 'm+': date.getMinutes(), // 分 21 | 's+': date.getSeconds(), // 秒 22 | 'q+': Math.floor((date.getMonth() + 3) / 3), // 季度 23 | 'S': date.getMilliseconds() // 毫秒 24 | } 25 | var week = { 26 | '0': '\u65e5', 27 | '1': '\u4e00', 28 | '2': '\u4e8c', 29 | '3': '\u4e09', 30 | '4': '\u56db', 31 | '5': '\u4e94', 32 | '6': '\u516d' 33 | } 34 | if (/(y+)/.test(fmt)) { 35 | fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) 36 | } 37 | if (/(E+)/.test(fmt)) { 38 | fmt = fmt.replace(RegExp.$1, ((RegExp.$1.length > 1) ? (RegExp.$1.length > 2 ? '\u661f\u671f' : '\u5468') : '') + week[date.getDay() + '']) 39 | } 40 | for (var k in o) { 41 | if (new RegExp('(' + k + ')').test(fmt)) { 42 | fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length))) 43 | } 44 | } 45 | return fmt 46 | } 47 | } 48 | } 49 | 50 | module.exports = new DateUtil().formatDate -------------------------------------------------------------------------------- /lib/FileUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-08-05 14:36:13 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-03 20:13:39 6 | * @Description: 7 | */ 8 | const getRealMainScriptPath = function (parentDirOnly) { 9 | let currentPath = files.cwd() 10 | if (files.exists(currentPath + '/main.js')) { 11 | return currentPath + (parentDirOnly ? '' : '/main.js') 12 | } 13 | let paths = currentPath.split('/') 14 | 15 | do { 16 | paths = paths.slice(0, paths.length - 1) 17 | currentPath = paths.reduce((a, b) => a += '/' + b) 18 | } while (!files.exists(currentPath + '/main.js') && paths.length > 0); 19 | if (paths.length > 0) { 20 | return currentPath + (parentDirOnly ? '' : '/main.js') 21 | } 22 | } 23 | 24 | /** 25 | * 获取当前脚本的运行工作路径,main.js所在的文件夹 26 | */ 27 | const getCurrentWorkPath = function () { 28 | return getRealMainScriptPath(true) 29 | } 30 | 31 | module.exports = { 32 | getRealMainScriptPath: getRealMainScriptPath, 33 | getCurrentWorkPath: getCurrentWorkPath 34 | } -------------------------------------------------------------------------------- /lib/FloatyUtil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-12-02 19:05:01 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-08 10:09:16 6 | * @Description: 悬浮窗工具,单独提出来作为单例使用 7 | */ 8 | let Timers = require('./Timers.js')(runtime, this) 9 | 10 | let FloatyUtil = function () { 11 | this.floatyWindow = null 12 | this.floatyInitStatus = false 13 | this.floatyLock = null 14 | this.floatyCondition = null 15 | } 16 | FloatyUtil.prototype.init = function () { 17 | this.floatyLock = threads.lock() 18 | this.floatyCondition = this.floatyLock.newCondition() 19 | let _this = this 20 | threads.start(function () { 21 | // 延迟初始化,避免死机 22 | sleep(400) 23 | _this.floatyLock.lock() 24 | try { 25 | _this.floatyWindow = floaty.rawWindow( 26 | 27 | 28 | 29 | ) 30 | _this.floatyWindow.setTouchable(false) 31 | _this.floatyWindow.setPosition(50, 50) 32 | _this.floatyWindow.content.text('悬浮窗初始化成功') 33 | _this.floatyInitStatus = true 34 | } catch (e) { 35 | console.error('悬浮窗初始化失败' + e) 36 | _this.floatyInitStatus = false 37 | } finally { 38 | _this.floatyCondition.signalAll() 39 | _this.floatyLock.unlock() 40 | } 41 | }) 42 | this.floatyLock.lock() 43 | if (this.floatyInitStatus === false) { 44 | console.verbose('等待悬浮窗初始化') 45 | this.floatyCondition.await() 46 | } 47 | this.floatyLock.unlock() 48 | console.verbose('悬浮窗初始化' + (this.floatyInitStatus ? '成功' : '失败')) 49 | return this.floatyInitStatus 50 | } 51 | 52 | FloatyUtil.prototype.close = function () { 53 | this.floatyLock.lock() 54 | this.floatyWindow.close() 55 | this.floatyWindow = null 56 | this.floatyInitStatus = false 57 | this.floatyLock.unlock() 58 | } 59 | 60 | FloatyUtil.prototype.setFloatyInfo = function (position, text) { 61 | if (this.floatyWindow === null) { 62 | this.init() 63 | } 64 | let _this = this 65 | ui.run(function () { 66 | _this.floatyLock.lock() 67 | if (position && isFinite(position.x) && isFinite(position.y)) { 68 | _this.floatyWindow.setPosition(parseInt(position.x), parseInt(position.y)) 69 | } 70 | if (text) { 71 | _this.floatyWindow.content.text(text) 72 | } 73 | _this.floatyLock.unlock() 74 | }) 75 | } 76 | 77 | 78 | FloatyUtil.prototype.setFloatyTextColor = function (colorStr) { 79 | if (this.floatyWindow === null) { 80 | this.init() 81 | } 82 | if (/#[\dabcdef]{6}/i.test(colorStr)) { 83 | let colorInt = colors.parseColor(colorStr) 84 | if (colorInt !== null) { 85 | let _this = this 86 | ui.run(function () { 87 | _this.floatyLock.lock() 88 | _this.floatyWindow.content.setTextColor(colorInt) 89 | _this.floatyLock.unlock() 90 | }) 91 | } 92 | } else { 93 | console.error('颜色值字符串格式不正确: ' + colorStr) 94 | } 95 | } 96 | 97 | module.exports = new FloatyUtil() -------------------------------------------------------------------------------- /lib/LogUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Last Modified by: TonyJiangWJ 4 | * @Last Modified time: 2019-12-04 13:07:28 5 | * @Description: 日志工具 6 | */ 7 | let _config = typeof config === 'undefined' ? require('../config.js').config : config 8 | let { storage_name } = require('../config.js') 9 | let formatDate = require('./DateUtil.js') 10 | let FileUtils = require('./FileUtils.js') 11 | let storage = storages.create(storage_name + 'run_log_file') 12 | 13 | /** 14 | * @param {string} content 15 | * @param {function} logFunc 执行控制台日志打印的方法 16 | * @param {boolean} isToast 17 | * @param {function} appendLogFunc 写入额外日志的方法 18 | * @param {string} prefix 日志的前缀 19 | */ 20 | const showToast = function (content, logFunc, isToast, appendLogFunc, prefix) { 21 | content = convertObjectContent(content) 22 | if (isToast) { 23 | toast(content) 24 | } 25 | innerAppendLog(content, appendLogFunc, prefix) 26 | logFunc(content) 27 | } 28 | 29 | const removeOutdateBacklogFiles = function () { 30 | let logbackDirPath = FileUtils.getRealMainScriptPath(true) + '/logs/logback' 31 | if (files.exists(logbackDirPath)) { 32 | // 日志保留天数 33 | let saveDays = _config.logSavedDays || 3 34 | let threeDayAgo = formatDate(new Date(new Date().getTime() - saveDays * 24 * 3600000), 'yyyyMMddHHmm') 35 | let timeRegex = /.*(\d{12})\.log/ 36 | let outdateLogs = files.listDir(logbackDirPath, function (fileName) { 37 | let checkResult = timeRegex.exec(fileName) 38 | if (checkResult) { 39 | let timestr = checkResult[1] 40 | return timestr < threeDayAgo 41 | } else { 42 | return true 43 | } 44 | }) 45 | if (outdateLogs && outdateLogs.length > 0) { 46 | outdateLogs.forEach(logFile => { 47 | console.verbose('日志文件过期,删除掉:' + logFile) 48 | files.remove(logbackDirPath + '/' + logFile) 49 | }) 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * 清除日志到备份文件夹,当不传递日志类型时清除所有日志 56 | * @param {string} target 日志类型 57 | */ 58 | const innerClearLogFile = function (target) { 59 | let mainScriptPath = FileUtils.getRealMainScriptPath(true) + '/logs' 60 | if (!target || target === 'verbose') { 61 | clearTarget(mainScriptPath, mainScriptPath + '/log-verboses.log', 'log-verboses') 62 | } 63 | if (!target || target === 'error') { 64 | clearTarget(mainScriptPath, mainScriptPath + '/error.log', 'error') 65 | } 66 | if (!target || target === 'log') { 67 | clearTarget(mainScriptPath, mainScriptPath + '/log.log', 'log') 68 | } 69 | if (!target || target === 'warn') { 70 | clearTarget(mainScriptPath, mainScriptPath + '/warn.log', 'warn') 71 | } 72 | if (!target || target === 'info') { 73 | clearTarget(mainScriptPath, mainScriptPath + '/info.log', 'info') 74 | } 75 | } 76 | 77 | const clearTarget = function (parentPath, filePath, fileName) { 78 | if (files.exists(filePath)) { 79 | let timestampLastHour = new Date().getTime() 80 | let backLogPath = parentPath + '/logback/' + fileName + '.' + formatDate(new Date(timestampLastHour), 'yyyyMMddHHmm') + '.log' 81 | files.ensureDir(parentPath + '/logback/') 82 | console.info('备份日志文件[' + backLogPath + ']' + (files.move(filePath, backLogPath) ? '成功' : '失败')) 83 | } else { 84 | console.info(filePath + '不存在,不执行备份') 85 | } 86 | try { 87 | files.write(filePath, fileName + ' logs for [' + formatDate(new Date()) + ']\n') 88 | } catch (e) { 89 | console.error('初始化写入日志文件失败' + e) 90 | } 91 | } 92 | 93 | const checkFileSizeAndBackup = function (filePath, target) { 94 | 95 | let clearFlag = storage.get(target) 96 | if (!clearFlag || parseInt(clearFlag) < new Date().getTime()) { 97 | // 十秒钟进行一次 98 | clearFlag = new Date().getTime() + 10000 99 | storage.put(target, clearFlag) 100 | 101 | let length = new java.io.File(filePath).length() 102 | if (files.exists(filePath) && length > 1024 * (_config.back_size || 100)) { 103 | console.verbose(target + '文件大小:' + length + ' 大于100kb,执行备份') 104 | innerClearLogFile(target) 105 | } 106 | } else { 107 | // 十秒内计算过当前类型的大小,不执行计算 108 | return 109 | } 110 | 111 | } 112 | 113 | const innerAppendLog = function (content, appendAnother, prefix) { 114 | if (_config.saveLogFile) { 115 | 116 | // 每个整点清除过期日志 117 | let compareDateTime = formatDate(new Date(), 'yyyyMMddHH') 118 | let last_back_file = storage.get('last_back_file') 119 | if (compareDateTime !== last_back_file) { 120 | console.verbose('old last_back_file: ' + last_back_file + ' new compare_flag:' + compareDateTime) 121 | storage.put('last_back_file', compareDateTime) 122 | removeOutdateBacklogFiles() 123 | } 124 | let string = formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss.S') + ':' + (prefix ? prefix : '') + content + '\n' 125 | files.ensureDir(FileUtils.getRealMainScriptPath(true) + '/logs/') 126 | let logFilePath = FileUtils.getRealMainScriptPath(true) + '/logs/log-verboses.log' 127 | try { 128 | checkFileSizeAndBackup(logFilePath, 'verbose') 129 | files.append(logFilePath, string) 130 | } catch (e) { 131 | console.error('写入日志信息失败' + e) 132 | } 133 | 134 | if (appendAnother) { 135 | try { 136 | appendAnother(string) 137 | } catch (e) { 138 | console.error('写入额外日志文件失败' + e) 139 | } 140 | } 141 | } 142 | } 143 | 144 | 145 | function convertObjectContent (originContent) { 146 | if (typeof originContent === 'string') { 147 | return originContent 148 | } else if (Array.isArray(originContent)) { 149 | // let [marker, ...args] = originContent 150 | let marker = originContent[0] 151 | let args = originContent.slice(1) 152 | let regex = /(\{\})/g 153 | let matchResult = marker.match(regex) 154 | if (matchResult && args && matchResult.length > 0 && matchResult.length === args.length) { 155 | args.forEach((item, idx) => { 156 | marker = marker.replace('{}', item) 157 | }) 158 | return marker 159 | } else if (matchResult === null) { 160 | return marker 161 | } 162 | } 163 | console.error('参数不匹配[' + originContent + ']') 164 | return originContent 165 | } 166 | 167 | 168 | module.exports = { 169 | debugInfo: function (content, isToast) { 170 | if (_config.show_debug_log) { 171 | showToast(content, (c) => console.verbose(c), isToast, null, '[DEBUG]') 172 | } else { 173 | content = convertObjectContent(content) 174 | innerAppendLog('[DEBUG]' + content) 175 | } 176 | }, 177 | logInfo: function (content, isToast) { 178 | showToast(content, (c) => console.log(c), isToast, 179 | (string) => { 180 | let filePath = FileUtils.getRealMainScriptPath(true) + '/logs/log.log' 181 | checkFileSizeAndBackup(filePath, 'log') 182 | files.append(filePath, string) 183 | }, '[LOG]' 184 | ) 185 | }, 186 | infoLog: function (content, isToast) { 187 | showToast(content, (c) => console.info(c), isToast, 188 | (string) => { 189 | let filePath = FileUtils.getRealMainScriptPath(true) + '/logs/info.log' 190 | checkFileSizeAndBackup(filePath, 'info') 191 | files.append(filePath, string) 192 | }, '[INFO]' 193 | ) 194 | }, 195 | warnInfo: function (content, isToast) { 196 | showToast(content, (c) => console.warn(c), isToast, 197 | (string) => { 198 | let filePath = FileUtils.getRealMainScriptPath(true) + '/logs/warn.log' 199 | checkFileSizeAndBackup(filePath, 'warn') 200 | files.append(filePath, string) 201 | }, '[WARN]' 202 | ) 203 | }, 204 | errorInfo: function (content, isToast) { 205 | showToast(content, (c) => console.error(c), isToast, 206 | (string) => { 207 | let filePath = FileUtils.getRealMainScriptPath(true) + '/logs/error.log' 208 | checkFileSizeAndBackup(filePath, 'error') 209 | files.append(filePath, string) 210 | }, '[ERROR]' 211 | ) 212 | }, 213 | appendLog: innerAppendLog, 214 | clearLogFile: innerClearLogFile, 215 | removeOldLogFiles: removeOutdateBacklogFiles 216 | } -------------------------------------------------------------------------------- /lib/RunningQueueDispatcher.js: -------------------------------------------------------------------------------- 1 | let STORAGE_KEY = "autojs_dispatch_queue_storage" 2 | let RUNNING_KEY = "qunningTask" 3 | let WAITING_QUEUE_KEY = "waitingQueue" 4 | let WRITE_LOCK_KEY = "writeLock" 5 | let Timers = require('./Timers.js')(runtime, this) 6 | 7 | let pwd = files.cwd() 8 | let logLibExists = files.exists(pwd + '/lib/LogUtils.js') 9 | let _logUtils = typeof LogUtils === 'undefined' && logLibExists ? require(pwd + '/lib/LogUtils.js') : { 10 | logInfo: (str) => { 11 | console.log(str) 12 | }, 13 | warnInfo: (str) => { 14 | console.warn(str) 15 | }, 16 | debugInfo: (str) => { 17 | console.verbose(str) 18 | }, 19 | infoLog: (str) => { 20 | console.info(str) 21 | }, 22 | errorInfo: (str) => { 23 | console.error(str) 24 | }, 25 | } 26 | if (logLibExists) { 27 | _logUtils.debugInfo('日志工具类存在') 28 | } else { 29 | _logUtils.debugInfo('日志工具类不存在, 当前目录:' + pwd) 30 | } 31 | 32 | 33 | function RunningQueueDispatcher () { 34 | 35 | this.checkDuplicateRunning = function () { 36 | let currentEngine = engines.myEngine() 37 | let runningEngines = engines.all() 38 | let runningSize = runningEngines.length 39 | let currentSource = currentEngine.getSource() + '' 40 | _logUtils.debugInfo('Dispatcher:当前脚本信息 id:' + currentEngine.id + ' source:' + currentSource + ' 运行中脚本数量:' + runningSize) 41 | if (runningSize > 1) { 42 | runningEngines.forEach(engine => { 43 | let compareEngine = engine 44 | let compareSource = compareEngine.getSource() + '' 45 | _logUtils.debugInfo('Dispatcher:对比脚本信息 id:' + compareEngine.id + ' source:' + compareSource) 46 | if (currentEngine.id !== compareEngine.id && compareSource === currentSource) { 47 | _logUtils.warnInfo('Dispatcher:脚本正在运行中 退出当前脚本:' + currentSource, true) 48 | this.removeRunningTask(true) 49 | engines.myEngine().forceStop() 50 | exit() 51 | } 52 | }) 53 | } 54 | } 55 | 56 | /** 57 | * 设置自动启动 58 | * 59 | * @param {string} source 脚本path路径 60 | * @param {number} seconds 延迟时间 秒 61 | */ 62 | this.setUpAutoStart = function (source, seconds) { 63 | let waitTime = seconds || 5 64 | let task = Timers.addDisposableTask({ 65 | path: source, 66 | date: new Date().getTime() + waitTime * 1000 67 | }) 68 | _logUtils.debugInfo("定时任务预定成功: " + task.id); 69 | } 70 | 71 | this.getCurrentTaskInfo = function () { 72 | currentEngine = engines.myEngine().getSource() + '' 73 | return { 74 | source: currentEngine, 75 | engineId: engines.myEngine().id 76 | } 77 | } 78 | 79 | 80 | this.clearAll = function () { 81 | storages.remove(STORAGE_KEY) 82 | _logUtils.logInfo('清除数据成功') 83 | } 84 | 85 | this.showDispatchStatus = function () { 86 | let runningTaskStr = this.getStorage().get(RUNNING_KEY) 87 | let waitingQueueStr = this.getStorage().get(WAITING_QUEUE_KEY) 88 | let lockKeyStr = this.getStorage().get(WRITE_LOCK_KEY) 89 | if (runningTaskStr) { 90 | let runningTask = JSON.parse(runningTaskStr) 91 | let timeout = new Date().getTime() - parseInt(runningTask.timeout) 92 | _logUtils.logInfo('当前运行中的任务:' + runningTaskStr + (timeout > 0 ? ' 已超时' + (timeout / 1000.0).toFixed(2) + '秒' : ' 超时剩余时间' + (-timeout / 1000.0).toFixed(0) + '秒')) 93 | } else { 94 | _logUtils.logInfo('当前无运行中的任务') 95 | } 96 | if (waitingQueueStr && waitingQueueStr !== '[]') { 97 | _logUtils.logInfo('当前等待中的队列:' + waitingQueueStr) 98 | } else { 99 | _logUtils.logInfo('当前无等待中的队列') 100 | } 101 | if (lockKeyStr) { 102 | let key = JSON.parse(lockKeyStr) 103 | _logUtils.logInfo('当前存在的锁:' + lockKeyStr + " 超时时间剩余:" + ((parseInt(key.timeout) - new Date().getTime()) / 1000.0).toFixed(2) + '秒') 104 | } else { 105 | _logUtils.logInfo('当前无存在的锁') 106 | } 107 | } 108 | 109 | this.getStorage = function () { 110 | return storages.create(STORAGE_KEY) 111 | } 112 | 113 | this.clearLock = function () { 114 | let taskInfo = this.getCurrentTaskInfo() 115 | let storedLockStr = this.getStorage().get(WRITE_LOCK_KEY) 116 | if (storedLockStr) { 117 | let storedLock = JSON.parse(storedLockStr) 118 | if (storedLock.source === taskInfo.source) { 119 | _logUtils.debugInfo('移除任务锁:' + JSON.stringify(taskInfo)) 120 | this.getStorage().put(WRITE_LOCK_KEY, '') 121 | } 122 | } 123 | } 124 | 125 | this.lock = function () { 126 | let taskInfo = this.getCurrentTaskInfo() 127 | let storedLockStr = this.getStorage().get(WRITE_LOCK_KEY) 128 | if (storedLockStr) { 129 | let storedLock = JSON.parse(storedLockStr) 130 | if (storedLock.source === taskInfo.source) { 131 | storedLock.count = parseInt(storedLock.count) + 1 132 | storedLock.timeout = new Date().getTime() + 30000 133 | this.getStorage().put(WRITE_LOCK_KEY, JSON.stringify(storedLock)) 134 | return true 135 | } else { 136 | if (parseInt(storedLock.timeout) < new Date().getTime()) { 137 | _logUtils.warnInfo('已有锁已超时,直接覆盖:' + JSON.stringify(storedLock)) 138 | this.getStorage().put(WRITE_LOCK_KEY, JSON.stringify({ source: taskInfo.source, count: 1, timeout: new Date().getTime() + 30000 })) 139 | return true 140 | } 141 | return false 142 | } 143 | } else { 144 | this.getStorage().put(WRITE_LOCK_KEY, JSON.stringify({ source: taskInfo.source, count: 1, timeout: new Date().getTime() + 30000 })) 145 | return true 146 | } 147 | } 148 | 149 | this.unlock = function () { 150 | let taskInfo = this.getCurrentTaskInfo() 151 | let storedLockStr = this.getStorage().get(WRITE_LOCK_KEY) 152 | if (storedLockStr) { 153 | let storedLock = JSON.parse(storedLockStr) 154 | if (storedLock.source === taskInfo.source) { 155 | if (parseInt(storedLock.count) > 1) { 156 | storedLock.count = parseInt(storedLock.count) - 1 157 | this.getStorage().put(WRITE_LOCK_KEY, JSON.stringify(storedLock)) 158 | } else { 159 | this.getStorage().put(WRITE_LOCK_KEY, '') 160 | } 161 | } else { 162 | return false 163 | } 164 | } else { 165 | return false 166 | } 167 | } 168 | 169 | this.getRunningStatus = function () { 170 | let storedRunningTask = this.getStorage().get(RUNNING_KEY) 171 | if (storedRunningTask) { 172 | let runningTask = JSON.parse(storedRunningTask) 173 | let currentTimestamp = new Date().getTime() 174 | if (currentTimestamp > runningTask.timeout) { 175 | _logUtils.debugInfo('运行中任务已超时:' + storedRunningTask + ' 超时时间:' + ((currentTimestamp - runningTask.timeout) / 1000).toFixed(0) + '秒') 176 | // 直接移除已超时运行中的任务 177 | this.getStorage().put(RUNNING_KEY, '') 178 | return null 179 | } else { 180 | _logUtils.debugInfo('获取运行中任务信息:' + storedRunningTask + ' 超时剩余时间:' + ((runningTask.timeout - currentTimestamp) / 1000).toFixed(0) + '秒') 181 | return runningTask 182 | } 183 | } else { 184 | return null 185 | } 186 | } 187 | 188 | this.getWaitingStatus = function () { 189 | // 任务队列去重 190 | this.distinctAwaitTasks() 191 | let waitingArrayStr = this.getStorage().get(WAITING_QUEUE_KEY) 192 | let waitingArray = null 193 | if (waitingArrayStr) { 194 | waitingArray = JSON.parse(waitingArrayStr) 195 | } 196 | if (waitingArray && waitingArray.length > 0) { 197 | return waitingArray[0] 198 | } 199 | return null 200 | } 201 | 202 | this.popWaitingTask = function () { 203 | let waitingArrayStr = this.getStorage().get(WAITING_QUEUE_KEY) 204 | let waitingArray = null 205 | if (waitingArrayStr) { 206 | waitingArray = JSON.parse(waitingArrayStr) 207 | } 208 | if (waitingArray && waitingArray.length > 0) { 209 | waitingArray.splice(0, 1) 210 | if (this.lock()) { 211 | this.getStorage().put(WAITING_QUEUE_KEY, JSON.stringify(waitingArray)) 212 | this.unlock() 213 | } 214 | } 215 | return null 216 | } 217 | 218 | /** 219 | * @param {boolean} checkOwner 判断当前运行中的任务信息是否是当前脚本引擎施加的 220 | */ 221 | this.removeRunningTask = function (checkOwner) { 222 | let taskInfo = this.getCurrentTaskInfo() 223 | let runningTask = this.getRunningStatus() 224 | if (runningTask !== null) { 225 | // engineId判断所有权 226 | if (runningTask.source === taskInfo.source && (!checkOwner || runningTask.engineId === taskInfo.engineId)) { 227 | _logUtils.debugInfo('移除' + (checkOwner ? '重复运行误创建的' : '') + '运行中任务') 228 | // 不加锁直接移除 229 | this.getStorage().put(RUNNING_KEY, '') 230 | 231 | let waitingTask = this.getWaitingStatus() 232 | if (waitingTask !== null && this.lock()) { 233 | _logUtils.debugInfo('有任务在等待,执行它') 234 | this.popWaitingTask() 235 | _logUtils.debugInfo('执行等待队列首个任务:' + JSON.stringify(waitingTask)) 236 | 237 | // 将队列中任务放入执行中 238 | this.doAddRunningTask(waitingTask) 239 | sleep(1000) 240 | // 将队列中的任务执行掉 241 | this.setUpAutoStart(waitingTask.source) 242 | this.unlock() 243 | } 244 | } else { 245 | _logUtils.warnInfo('运行中任务:' + JSON.stringify(runningTask) + '和当前任务:' + JSON.stringify(taskInfo) + '不同,不可移除') 246 | } 247 | } else { 248 | _logUtils.warnInfo('无任务在运行中,不可移除') 249 | } 250 | // 清空当前任务施加的锁 251 | this.clearLock() 252 | } 253 | 254 | this.doAddRunningTask = function (taskInfo) { 255 | // 默认超时时间15分钟 256 | taskInfo.timeout = new Date().getTime() + 15 * 60 * 1000 257 | this.getStorage().put(RUNNING_KEY, JSON.stringify(taskInfo)) 258 | } 259 | 260 | this.addRunningTask = function () { 261 | let taskInfo = this.getCurrentTaskInfo() 262 | let runningTask = this.getRunningStatus() 263 | if (runningTask !== null) { 264 | _logUtils.debugInfo('当前有任务正在运行:' + JSON.stringify(runningTask)) 265 | if (runningTask.source === taskInfo.source) { 266 | _logUtils.debugInfo('运行中脚本任务和当前任务相同,直接继续') 267 | // 如果判断当前运行中和存储任务状态是同一个则不去校验是否重复运行 268 | if (runningTask.engineId !== taskInfo.engineId) { 269 | // 避免重复运行,如果挂了则继续 270 | this.checkDuplicateRunning() 271 | } 272 | return 273 | } else { 274 | _logUtils.debugInfo('将当前task放入等待队列:' + JSON.stringify(taskInfo)) 275 | this.addAwaitTask(taskInfo) 276 | exit() 277 | } 278 | } else { 279 | let waitingTask = this.getWaitingStatus() 280 | if (waitingTask !== null) { 281 | _logUtils.debugInfo('等待队列中已有任务待运行:' + JSON.stringify(waitingTask)) 282 | if (waitingTask.source === taskInfo.source) { 283 | _logUtils.debugInfo('等待中任务和当前任务相同,可直接执行,将任务信息放入running') 284 | if (this.lock()) { 285 | this.doAddRunningTask(taskInfo) 286 | this.popWaitingTask() 287 | this.unlock() 288 | } else { 289 | _logUtils.errorInfo('获取锁失败,无法继续执行任务:' + JSON.stringify(taskInfo)) 290 | this.setUpAutoStart(taskInfo.source, 10) 291 | exit() 292 | } 293 | } else { 294 | _logUtils.debugInfo('等待中任务和当前任务不同,将任务信息放入等待队列:' + JSON.stringify(taskInfo)) 295 | if (this.lock()) { 296 | this.addAwaitTask(taskInfo) 297 | this.popWaitingTask() 298 | _logUtils.debugInfo('执行等待队列首个任务:' + JSON.stringify(waitingTask)) 299 | // 将队列中任务放入执行中 300 | this.doAddRunningTask(waitingTask) 301 | // 将队列中的任务执行掉 302 | this.setUpAutoStart(waitingTask.source) 303 | this.unlock() 304 | exit() 305 | } else { 306 | _logUtils.errorInfo('获取锁失败,无法执行等待中任务,当前任务也未成功入队列:' + JSON.stringify(taskInfo)) 307 | this.setUpAutoStart(taskInfo.source, 10) 308 | exit() 309 | } 310 | } 311 | } else { 312 | if (this.lock()) { 313 | _logUtils.debugInfo('当前无任务等待,直接执行:' + JSON.stringify(taskInfo)) 314 | this.doAddRunningTask(taskInfo) 315 | this.unlock() 316 | } else { 317 | _logUtils.errorInfo('获取锁失败,无法继续执行任务:' + JSON.stringify(taskInfo)) 318 | this.setUpAutoStart(taskInfo.source, 10) 319 | exit() 320 | } 321 | } 322 | } 323 | } 324 | 325 | this.addAwaitTask = function (taskInfo) { 326 | let storedArrayStr = this.getStorage().get(WAITING_QUEUE_KEY) 327 | let storedArray = null 328 | if (storedArrayStr) { 329 | storedArray = JSON.parse(storedArrayStr) 330 | } else { 331 | storedArray = [] 332 | } 333 | storedArray.push(taskInfo) 334 | if (this.lock()) { 335 | this.getStorage().put(WAITING_QUEUE_KEY, JSON.stringify(storedArray)) 336 | this.distinctAwaitTasks() 337 | this.unlock() 338 | } else { 339 | _logUtils.errorInfo('添加等待任务队列失败,获取写锁失败,任务信息:' + JSON.stringify(taskInfo)) 340 | this.setUpAutoStart(taskInfo.source, 10) 341 | } 342 | } 343 | 344 | this.distinctAwaitTasks = function () { 345 | if (this.lock()) { 346 | let storedArrayStr = this.getStorage().get(WAITING_QUEUE_KEY) 347 | let storedArray = null 348 | if (storedArrayStr) { 349 | storedArray = JSON.parse(storedArrayStr) 350 | } else { 351 | storedArray = [] 352 | } 353 | if (storedArray && storedArray.length > 0) { 354 | _logUtils.debugInfo('去重复前的任务队列:' + storedArrayStr) 355 | let distinctArray = [] 356 | storedArray.forEach(task => { 357 | if (distinctArray.map(r => r.source).indexOf(task.source) < 0) { 358 | distinctArray.push(task) 359 | } 360 | }) 361 | let distinctArrayStr = JSON.stringify(distinctArray) 362 | _logUtils.debugInfo('去重复后的任务队列:' + distinctArrayStr) 363 | this.getStorage().put(WAITING_QUEUE_KEY, distinctArrayStr) 364 | } else { 365 | _logUtils.debugInfo('队列小于等于1 不需要去重:' + storedArrayStr) 366 | } 367 | this.unlock() 368 | } 369 | } 370 | 371 | } 372 | 373 | module.exports = new RunningQueueDispatcher() -------------------------------------------------------------------------------- /lib/Timers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: SilvMonFr.00 3 | * @Last Modified by: TonyJiangWJ 4 | * @Last Modified time: 2019-07-04 00:14:22 5 | * @Description: 定时任务桥接,由大佬SilvMonFr.00提供 6 | */ 7 | importPackage(org.joda.time); 8 | 9 | let { getVerName, waitForAction } = files.exists("./MODULE_MONSTER_FUNC") ? require("./MODULE_MONSTER_FUNC") : { 10 | getVerName: _getVerName, 11 | waitForAction: _waitForAction, 12 | }; 13 | 14 | module.exports = function (runtime, scope) { 15 | 16 | let is_pro = getVerName("current_autojs").match(/[Pp]ro/); 17 | let timing = is_pro ? com.stardust.autojs.core.timing : org.autojs.autojs.timing; 18 | var timers = Object.create(runtime.timers); 19 | var TimedTask = is_pro ? timing.TimedTask.Companion : timing.TimedTask; 20 | var IntentTask = timing.IntentTask; 21 | var TimedTaskManager = is_pro ? timing.TimedTaskManager.Companion.getInstance() : timing.TimedTaskManager.getInstance(); 22 | var bridges = require("__bridges__"); 23 | let days_ident = [ 24 | 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 25 | 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 26 | '一', '二', '三', '四', '五', '六', '日', 27 | 1, 2, 3, 4, 5, 6, 0, 28 | 1, 2, 3, 4, 5, 6, 7, 29 | ].map(value => value.toString()); 30 | 31 | scope.__asGlobal__(timers, ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'setImmediate', 'clearImmediate']); 32 | 33 | scope.loop = function () { 34 | console.warn("loop() has been deprecated and has no effect. Remove it from your code."); 35 | }; 36 | 37 | timers.addDailyTask = function (task) { 38 | let localTime = parseDateTime("LocalTime", task.time); 39 | let timedTask = TimedTask.dailyTask(localTime, files.path(task.path), parseConfig(task)); 40 | 41 | addTask(timedTask); 42 | return timedTask; 43 | }; 44 | 45 | timers.addWeeklyTask = function (task) { 46 | let localTime = parseDateTime("LocalTime", task.time); 47 | let timeFlag = 0; 48 | for (let i = 0; i < task.daysOfWeek.length; i++) { 49 | let dayString = task.daysOfWeek[i].toString(); 50 | let dayIndex = days_ident.indexOf(dayString.toLowerCase()) % 7; 51 | if (!~dayIndex) throw new Error('unknown day: ' + dayString); 52 | timeFlag |= TimedTask.getDayOfWeekTimeFlag(dayIndex + 1); 53 | } 54 | let timedTask = TimedTask.weeklyTask(localTime, new java.lang.Long(timeFlag), files.path(task.path), parseConfig(task)); 55 | addTask(timedTask); 56 | return timedTask; 57 | }; 58 | 59 | timers.addDisposableTask = function (task) { 60 | let localDateTime = parseDateTime("LocalDateTime", task.date); 61 | let timedTask = TimedTask.disposableTask(localDateTime, files.path(task.path), parseConfig(task)); 62 | addTask(timedTask); 63 | return timedTask; 64 | }; 65 | 66 | timers.addIntentTask = function (task) { 67 | let intentTask = new IntentTask(); 68 | intentTask.setScriptPath(files.path(task.path)); 69 | task.action && intentTask.setAction(task.action); 70 | addTask(intentTask); 71 | return intentTask; 72 | }; 73 | 74 | timers.getTimedTask = function (id) { 75 | return TimedTaskManager.getTimedTask(id); 76 | }; 77 | 78 | timers.getIntentTask = function (id) { 79 | return TimedTaskManager.getIntentTask(id); 80 | }; 81 | 82 | timers.removeIntentTask = function (id) { 83 | if (!id && isNaN(+id)) return; 84 | let task = timers.getIntentTask(id); 85 | return task && removeTask(task); 86 | }; 87 | 88 | timers.removeTimedTask = function (id) { 89 | if (!id && isNaN(+id)) return; 90 | let task = timers.getTimedTask(id); 91 | return task && removeTask(task); 92 | }; 93 | 94 | timers.queryTimedTasks = function (options, callback) { 95 | options = options || {}; 96 | var sql = ''; 97 | var args = []; 98 | 99 | function sqlAppend(str) { 100 | if (sql.length === 0) { 101 | sql += str; 102 | } else { 103 | sql += ' AND ' + str; 104 | } 105 | return true; 106 | } 107 | 108 | let path = options.path; 109 | path && sqlAppend('script_path = ?') && args.push(path); 110 | return is_pro ? bridges.toArray(TimedTaskManager.queryTimedTasks(sql || null, args)) : (() => { 111 | let list = TimedTaskManager.getAllTasksAsList().toArray(); 112 | if (options.path) list = list.filter(task => task.getScriptPath() === path); 113 | return list; 114 | })(); 115 | }; 116 | 117 | timers.queryIntentTasks = function (options, callback) { 118 | var sql = ''; 119 | var args = []; 120 | 121 | function sqlAppend(str) { 122 | if (sql.length === 0) { 123 | sql += str; 124 | } else { 125 | sql += ' AND ' + str; 126 | } 127 | return true; 128 | } 129 | 130 | options.path && sqlAppend('script_path = ?') && args.push(options.path); 131 | options.action && sqlAppend('action = ?') && args.push(options.action); 132 | return bridges.toArray(TimedTaskManager.queryIntentTasks(sql ? sql : null, args)); 133 | }; 134 | 135 | return timers; 136 | 137 | // tool function(s) // 138 | 139 | function parseConfig(c) { 140 | let config = new com.stardust.autojs.execution.ExecutionConfig(); 141 | config.delay = c.delay || 0; 142 | config.interval = c.interval || 0; 143 | config.loopTimes = (c.loopTimes === undefined) ? 1 : c.loopTimes; 144 | return config; 145 | } 146 | 147 | function parseDateTime(clazz, dateTime) { 148 | clazz = is_pro ? clazz : org.joda.time[clazz]; 149 | if (typeof (dateTime) == 'string') { 150 | return is_pro ? TimedTask.parseDateTime(clazz, dateTime) : clazz.parse(dateTime); 151 | } else if (typeof (dateTime) == 'object' && dateTime.constructor === Date) { 152 | return is_pro ? TimedTask.parseDateTime(clazz, dateTime.getTime()) : new clazz(dateTime.getTime()); 153 | } else if (typeof (dateTime) == 'number' && isFinite(dateTime)) { 154 | return is_pro ? TimedTask.parseDateTime(clazz, dateTime) : new clazz(dateTime); 155 | } else { 156 | throw new Error("cannot parse date time: " + dateTime); 157 | } 158 | } 159 | 160 | function addTask(task) { 161 | TimedTaskManager[is_pro ? "addTaskSync" : "addTask"](task); 162 | waitForAction(() => task.id !== 0, 500, 80); 163 | } 164 | 165 | function removeTask(task) { 166 | let id = task.id; 167 | TimedTaskManager[is_pro ? "removeTaskSync" : "removeTask"](task); 168 | return waitForAction(() => !timers.getTimedTask(id), 500, 80); 169 | } 170 | }; 171 | 172 | // monster function(s) // 173 | 174 | function _waitForAction(f, timeout_or_times, interval) { 175 | let _timeout = timeout_or_times || 10000; 176 | let _interval = interval || 200; 177 | let _times = _timeout < 100 ? _timeout : ~~(_timeout / _interval) + 1; 178 | 179 | let _messageAction = typeof messageAction === "undefined" ? messageActionRaw : messageAction; 180 | 181 | while (!_checkF(f) && _times--) sleep(_interval); 182 | return _times >= 0; 183 | 184 | // tool function(s) // 185 | 186 | function _checkF(f) { 187 | let _classof = o => Object.prototype.toString.call(o).slice(8, -1); 188 | if (_classof(f) === "JavaObject") return _checkF(() => f.exists()); 189 | if (_classof(f) === "Array") { 190 | let _arr = f; 191 | let _logic_flag = "all"; 192 | if (typeof _arr[_arr.length - 1] === "string") _logic_flag = _arr.pop(); 193 | if (_logic_flag.match(/^(or|one)$/)) _logic_flag = "one"; 194 | for (let i = 0, len = _arr.length; i < len; i += 1) { 195 | if (!(typeof _arr[i]).match(/function|object/)) _messageAction("数组参数中含不合法元素", 8, 1, 0, 1); 196 | if (_logic_flag === "all" && !_checkF(_arr[i])) return false; 197 | if (_logic_flag === "one" && _checkF(_arr[i])) return true; 198 | } 199 | return _logic_flag === "all"; 200 | } else if (typeof f === "function") return f(); 201 | else _messageAction("\"waitForAction\"传入f参数不合法\n\n" + f.toString() + "\n", 8, 1, 1, 1); 202 | } 203 | 204 | // raw function(s) // 205 | 206 | function messageActionRaw(msg, msg_level, toast_flag) { 207 | let _msg = msg || " "; 208 | if (msg_level && msg_level.toString().match(/^t(itle)?$/)) { 209 | return messageAction("[ " + msg + " ]", 1, toast_flag); 210 | } 211 | let _msg_level = typeof +msg_level === "number" ? +msg_level : -1; 212 | toast_flag && toast(_msg); 213 | _msg_level === 1 && log(_msg) || _msg_level === 2 && console.info(_msg) || 214 | _msg_level === 3 && console.warn(_msg) || _msg_level >= 4 && console.error(_msg); 215 | _msg_level >= 8 && exit(); 216 | return !(_msg_level in { 3: 1, 4: 1 }); 217 | } 218 | } 219 | 220 | function _getVerName(name, params) { 221 | 222 | let _params = params; 223 | 224 | let _parseAppName = typeof parseAppName === "undefined" ? parseAppNameRaw : parseAppName; 225 | let _debugInfo = _msg => (typeof debugInfo === "undefined" ? debugInfoRaw : debugInfo)(_msg, _params.debug_info_flag); 226 | 227 | name = _handleName(name); 228 | let _package_name = _parseAppName(name).package_name; 229 | if (!_package_name) return null; 230 | 231 | try { 232 | let _installed_packages = context.getPackageManager().getInstalledPackages(0).toArray(); 233 | for (let i in _installed_packages) { 234 | if (_installed_packages[i].packageName.toString() === _package_name.toString()) { 235 | return _installed_packages[i].versionName; 236 | } 237 | } 238 | } catch (e) { 239 | _debugInfo(e); 240 | } 241 | return null; 242 | 243 | // tool function(s) // 244 | 245 | function _handleName(name) { 246 | if (name.match(/^[Aa]uto\.?js/)) return "org.autojs.autojs" + (name.match(/[Pp]ro$/) ? "pro" : ""); 247 | if (name === "self") return currentPackage(); 248 | if (name.match(/^[Cc]urrent.*[Aa]uto.*js/)) return context.packageName; 249 | return name; 250 | } 251 | 252 | // raw function(s) // 253 | 254 | function debugInfoRaw(msg, info_flag) { 255 | if (info_flag) console.verbose((msg || "").replace(/^(>*)( *)/, ">>" + "$1 ")); 256 | } 257 | 258 | function parseAppNameRaw(name) { 259 | let _app_name = !name.match(/.+\..+\./) && app.getPackageName(name) && name; 260 | let _package_name = app.getAppName(name) && name; 261 | _app_name = _app_name || _package_name && app.getAppName(_package_name); 262 | _package_name = _package_name || _app_name && app.getPackageName(_app_name); 263 | return { 264 | app_name: _app_name, 265 | package_name: _package_name, 266 | }; 267 | } 268 | } -------------------------------------------------------------------------------- /lib/TryRequestScreenCapture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Just an insurance way of images.requestScreenCapture() to avoid infinite stuck or stalled without any hint or log 3 | * During this operation, permission prompt window will be confirmed (with checkbox checked if possible) automatically with effort 4 | * @param [params] {object} 5 | * @param [params.debug_info_flag] {boolean} 6 | * @param [params.restart_this_engine_flag=false] {boolean} 7 | * @param [params.restart_this_engine_params] {object} 8 | * @param [params.restart_this_engine_params.new_file] {string} - new engine task name with or without path or file extension name 9 | *
10 | * -- *DEFAULT* - old engine task
11 | * -- new file - like "hello.js", "../hello.js" or "hello" 12 | * @param [params.restart_this_engine_params.debug_info_flag] {boolean} 13 | * @param [params.restart_this_engine_params.max_restart_engine_times=3] {number} - max restart times for avoiding infinite recursion 14 | * @return {boolean} 15 | */ 16 | function tryRequestScreenCapture(params) { 17 | __global__ = typeof __global__ === "undefined" ? {} : __global__; 18 | if (__global__._monster_$_request_screen_capture_flag) return true; 19 | 20 | sleep(200); // why are you always a naughty boy... how can i get along well with you... 21 | 22 | let _params = params || {}; 23 | 24 | let _debugInfo = _msg => (typeof debugInfo === "undefined" ? debugInfoRaw : debugInfo)(_msg, "", _params.debug_info_flag); 25 | let _waitForAction = typeof waitForAction === "undefined" ? waitForActionRaw : waitForAction; 26 | let _messageAction = typeof messageAction === "undefined" ? messageActionRaw : messageAction; 27 | let _clickAction = typeof clickAction === "undefined" ? clickActionRaw : clickAction; 28 | let _restartThisEngine = typeof restartThisEngine === "undefined" ? restartThisEngineRaw : restartThisEngine; 29 | let _getSelector = typeof getSelector === "undefined" ? getSelectorRaw : getSelector; 30 | 31 | let sel = _getSelector(); 32 | 33 | _params.restart_this_engine_flag = typeof _params.restart_this_engine_flag === "undefined" ? true : !!_params.restart_this_engine_flag; 34 | _params.restart_this_engine_params = _params.restart_this_engine_params || {}; 35 | _params.restart_this_engine_params.max_restart_engine_times = _params.restart_this_engine_params.max_restart_engine_times || 3; 36 | 37 | _debugInfo("开始申请截图权限"); 38 | 39 | __global__._monster_$_request_screen_capture_flag = true; 40 | _debugInfo("已存储截图权限申请标记"); 41 | 42 | _debugInfo("已开启弹窗监测线程"); 43 | let _thread_prompt = threads.start(function () { 44 | let _kw_no_longer_prompt = type => sel.pickup(id("com.android.systemui:id/remember"), "kw_req_capt_no_longer_prompt", type); 45 | let _kw_sure_btn = type => sel.pickup(/START NOW|立即开始|允许/, "", type); 46 | 47 | if (_waitForAction(_kw_sure_btn, 5000)) { 48 | if (_waitForAction(_kw_no_longer_prompt, 1000)) { 49 | _debugInfo("勾选\"不再提示\"复选框"); 50 | _clickAction(_kw_no_longer_prompt(), "widget"); 51 | } 52 | if (_waitForAction(_kw_sure_btn, 2000)) { 53 | let _node = _kw_sure_btn(); 54 | let _btn_click_action_str = "点击\"" + _kw_sure_btn("txt") + "\"按钮"; 55 | 56 | _debugInfo(_btn_click_action_str); 57 | _clickAction(_node, "widget"); 58 | 59 | if (!_waitForAction(() => !_kw_sure_btn(), 1000)) { 60 | _debugInfo("尝试click()方法再次" + _btn_click_action_str); 61 | _clickAction(_node, "click"); 62 | } 63 | } 64 | } 65 | }); 66 | 67 | let _thread_monitor = threads.start(function () { 68 | if (_waitForAction(() => !!_req_result, 2000, 500)) { 69 | _thread_prompt.interrupt(); 70 | return _debugInfo("截图权限申请结果: " + _req_result); 71 | } 72 | if (!__global__._monster_$_debug_info_flag) { 73 | __global__._monster_$_debug_info_flag = true; 74 | _debugInfo("开发者测试模式已自动开启", 3); 75 | } 76 | if (_params.restart_this_engine_flag) { 77 | _debugInfo("截图权限申请结果: 失败", 3); 78 | if (_restartThisEngine(_params.restart_this_engine_params)) return; 79 | } 80 | _messageAction("截图权限申请失败", 9, 1, 0, 1); 81 | }); 82 | 83 | let _req_result = images.requestScreenCapture(false); 84 | sleep(300); 85 | 86 | _thread_monitor.join(2400); 87 | _thread_monitor.interrupt(); 88 | return _req_result; 89 | 90 | // raw function(s) // 91 | 92 | function getSelectorRaw() { 93 | let classof = o => Object.prototype.toString.call(o).slice(8, -1); 94 | let sel = selector(); 95 | sel.__proto__ = { 96 | pickup: (filter) => { 97 | if (classof(filter) === "JavaObject") { 98 | if (filter.toString().match(/UiObject/)) return filter; 99 | return filter.findOnce() || null; 100 | } 101 | if (typeof filter === "string") return desc(filter).findOnce() || text(filter).findOnce() || null; 102 | if (classof(filter) === "RegExp") return descMatches(filter).findOnce() || textMatches(filter).findOnce() || null; 103 | return null; 104 | }, 105 | }; 106 | return sel; 107 | } 108 | 109 | function debugInfoRaw(msg, info_flag) { 110 | if (info_flag) console.verbose((msg || "").replace(/^(>*)( *)/, ">>" + "$1 ")); 111 | } 112 | 113 | function waitForActionRaw(cond_func, time_params) { 114 | let _cond_func = cond_func; 115 | if (!cond_func) return true; 116 | let classof = o => Object.prototype.toString.call(o).slice(8, -1); 117 | if (classof(cond_func) === "JavaObject") _cond_func = () => cond_func.exists(); 118 | let _check_time = typeof time_params === "object" && time_params[0] || time_params || 10000; 119 | let _check_interval = typeof time_params === "object" && time_params[1] || 200; 120 | while (!_cond_func() && _check_time >= 0) { 121 | sleep(_check_interval); 122 | _check_time -= _check_interval; 123 | } 124 | return _check_time >= 0; 125 | } 126 | 127 | function clickActionRaw(kw) { 128 | let classof = o => Object.prototype.toString.call(o).slice(8, -1); 129 | let _kw = classof(_kw) === "Array" ? kw[0] : kw; 130 | let _key_node = classof(_kw) === "JavaObject" && _kw.toString().match(/UiObject/) ? _kw : _kw.findOnce(); 131 | if (!_key_node) return; 132 | let _bounds = _key_node.bounds(); 133 | click(_bounds.centerX(), _bounds.centerY()); 134 | return true; 135 | } 136 | 137 | function messageActionRaw(msg, msg_level, toast_flag) { 138 | let _msg = msg || " "; 139 | if (msg_level && msg_level.toString().match(/^t(itle)?$/)) { 140 | return messageActionRaw("[ " + msg + " ]", 1, toast_flag); 141 | } 142 | let _msg_level = typeof +msg_level === "number" ? +msg_level : -1; 143 | toast_flag && toast(_msg); 144 | if (_msg_level === 0) return console.verbose(_msg) || true; 145 | if (_msg_level === 1) return console.log(_msg) || true; 146 | if (_msg_level === 2) return console.info(_msg) || true; 147 | if (_msg_level === 3) return console.warn(_msg) || false; 148 | if (_msg_level >= 4) { 149 | console.error(_msg); 150 | _msg_level >= 8 && exit(); 151 | } 152 | } 153 | 154 | function restartThisEngineRaw(params) { 155 | let _params = params || {}; 156 | let _my_engine = engines.myEngine(); 157 | 158 | let _max_restart_engine_times_argv = _my_engine.execArgv.max_restart_engine_times; 159 | let _max_restart_engine_times_params = _params.max_restart_engine_times; 160 | let _max_restart_engine_times; 161 | let _instant_run_flag = !!_params.instant_run_flag; 162 | if (typeof _max_restart_engine_times_argv === "undefined") { 163 | if (typeof _max_restart_engine_times_params === "undefined") _max_restart_engine_times = 1; 164 | else _max_restart_engine_times = +_max_restart_engine_times_params; 165 | } else _max_restart_engine_times = +_max_restart_engine_times_argv; 166 | 167 | if (!_max_restart_engine_times) return; 168 | 169 | let _file_name = _params.new_file || _my_engine.source.toString(); 170 | if (_file_name.match(/^\[remote]/)) return ~console.error("远程任务不支持重启引擎") && exit(); 171 | let _file_path = files.path(_file_name.match(/\.js$/) ? _file_name : (_file_name + ".js")); 172 | engines.execScriptFile(_file_path, { 173 | arguments: { 174 | max_restart_engine_times: _max_restart_engine_times - 1, 175 | instant_run_flag: _instant_run_flag, 176 | }, 177 | }); 178 | _my_engine.forceStop(); 179 | } 180 | } 181 | 182 | module.exports = { 183 | tryRequestScreenCapture: tryRequestScreenCapture 184 | } -------------------------------------------------------------------------------- /lib/Unlock.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-11-05 09:12:00 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-05 14:57:45 6 | * @Description: 7 | */ 8 | let _config = typeof config === 'undefined' ? require('../config.js').config : config 9 | let automator = require('./Automator.js') 10 | let Devices = { 11 | XIAOMI_MIX2S: function (obj) { 12 | this.__proto__ = obj 13 | // 图形密码解锁 14 | this.unlock_pattern = function (password) { 15 | if (typeof password !== 'string') throw new Error('密码应为字符串!') 16 | let pattern_view = id('com.android.systemui:id/lockPatternView') 17 | .findOne(_config.timeout_findOne) 18 | .bounds(), 19 | pattern_size = 3, 20 | len = password.length, 21 | view_x = pattern_view.left, 22 | view_y = pattern_view.top, 23 | width = (pattern_view.right - pattern_view.left) / pattern_size, 24 | height = (pattern_view.bottom - pattern_view.top) / pattern_size, 25 | points = [], 26 | ges_param = [] 27 | // 记录图形点信息 28 | for (let i = 0; i < pattern_size; i++) { 29 | for (let j = 0; j < pattern_size; j++) { 30 | let index = pattern_size * i + (j + 1) 31 | points[index] = [ 32 | parseInt(view_x + j * width + width / 2), 33 | parseInt(view_y + i * height + height / 2) 34 | ] 35 | } 36 | } 37 | // 构造滑动参数 38 | for (let i = 0; i < len; i++) ges_param.push(points[password[i]]) 39 | // 使用手势解锁 40 | automator.gesture(300 * len, ges_param) 41 | return this.check_unlock() 42 | } 43 | 44 | // 密码解锁(仅ROOT可用) 45 | this.unlock_password = function (password) { 46 | if (typeof password !== 'string') throw new Error('密码应为字符串!') 47 | // 直接在控件中输入密码 48 | setText(0, password) 49 | // 执行确认操作 50 | KeyCode('KEYCODE_ENTER') 51 | return this.check_unlock() 52 | } 53 | 54 | // PIN解锁 55 | this.unlock_pin = function (password) { 56 | if (typeof password !== 'string') throw new Error('密码应为字符串!') 57 | // 模拟按键 58 | let button = null 59 | for (let i = 0; i < password.length; i++) { 60 | let key_id = 'com.android.systemui:id/key' + password[i] 61 | if ((button = id(key_id).findOne(_config.timeout_findOne)) !== null) { 62 | button.click() 63 | } 64 | sleep(100) 65 | } 66 | return this.check_unlock() 67 | } 68 | 69 | // 判断解锁方式并解锁 70 | this.unlock = function (password) { 71 | if (id('com.android.systemui:id/lockPatternView').exists()) { 72 | return this.unlock_pattern(password) 73 | } else if (id('com.android.systemui:id/passwordEntry').exists()) { 74 | return this.unlock_password(password) 75 | } else if (id('com.android.systemui:id/pinEntry').exists()) { 76 | return this.unlock_pin(password) 77 | } else { 78 | logInfo( 79 | '识别锁定方式失败,型号:' + device.brand + ' ' + device.product + ' ' + device.release 80 | ) 81 | return this.check_unlock() 82 | } 83 | } 84 | } 85 | } 86 | 87 | const MyDevice = Devices.XIAOMI_MIX2S 88 | 89 | function Unlocker () { 90 | const _device = new MyDevice(this), 91 | _HEIGHT = device.height, 92 | _WIDTH = device.width, 93 | _km = context.getSystemService(context.KEYGUARD_SERVICE) 94 | 95 | this.relock = false 96 | this.reTry = 0 97 | 98 | // 设备是否锁屏 99 | this.is_locked = function () { 100 | return _km.inKeyguardRestrictedInputMode() 101 | } 102 | 103 | // 设备是否加密 104 | this.is_passwd = function () { 105 | return _km.isKeyguardSecure() 106 | } 107 | 108 | // 解锁失败 109 | this.failed = function () { 110 | automator.back() 111 | this.reTry++ 112 | if (this.reTry > 3) { 113 | logInfo('解锁失败达到三次,停止运行') 114 | device.setBrightnessMode(1) 115 | engines.myEngine().forceStop() 116 | } else { 117 | let sleepMs = 5000 * this.reTry 118 | logInfo('解锁失败,' + sleepMs + 'ms之后重试') 119 | sleep(sleepMs) 120 | this.run_unlock() 121 | } 122 | } 123 | 124 | // 检测是否解锁成功 125 | this.check_unlock = function () { 126 | sleep(_config.timeout_unlock) 127 | if ( 128 | textContains('重新').exists() || 129 | textContains('重试').exists() || 130 | textContains('错误').exists() 131 | ) { 132 | logInfo('密码错误') 133 | return false 134 | } 135 | return !this.is_locked() 136 | } 137 | 138 | // 唤醒设备 139 | this.wakeup = function () { 140 | while (!device.isScreenOn()) { 141 | device.wakeUp() 142 | sleep(_config.timeout_unlock) 143 | } 144 | } 145 | 146 | // 划开图层 147 | this.swipe_layer = function () { 148 | // let x = _WIDTH / 2; 149 | // let y = _HEIGHT / 6; 150 | // gesture(320, [x, 5 * y], [x, 1 * y]) 151 | // 暂时写死 否则兼容性较差 152 | gesture(320, [540, 1800], [540, 1000]) 153 | sleep(_config.timeout_unlock) 154 | } 155 | 156 | // 执行解锁操作 157 | this.run_unlock = function () { 158 | // 如果已经解锁则返回 159 | if (!this.is_locked()) { 160 | logInfo('已解锁') 161 | if (this.relock === true) { 162 | logInfo('前置校验需要重新锁定屏幕') 163 | } else { 164 | logInfo('不需要重新锁定屏幕') 165 | this.relock = false 166 | } 167 | return true 168 | } 169 | this.relock = true 170 | logInfo('需要重新锁定屏幕') 171 | if (_config.autoSetBrightness) { 172 | // 设置最低亮度 同时关闭自动亮度 173 | device.setBrightnessMode(0) 174 | device.setBrightness(0) 175 | } 176 | // 首先点亮屏幕 177 | this.wakeup() 178 | // 打开滑动层 179 | this.swipe_layer() 180 | // 如果有锁屏密码则输入密码 181 | if (this.is_passwd() && !_device.unlock(_config.password)) { 182 | // 如果解锁失败 183 | this.failed() 184 | } 185 | } 186 | } 187 | 188 | const _unlocker = new Unlocker() 189 | module.exports = { 190 | exec: function () { 191 | _unlocker.reTry = 0 192 | _unlocker.run_unlock() 193 | }, 194 | needRelock: function () { 195 | logInfo('是否需要重新锁定屏幕:' + _unlocker.relock) 196 | let tmp = _unlocker.relock 197 | _unlocker.relock = false 198 | return tmp 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /lib/WidgetUtils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-11-05 09:12:00 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-10 13:52:03 6 | * @Description: 7 | */ 8 | let _config = typeof config === 'undefined' ? require('../config.js').config : config 9 | let _commonFunctions = typeof commonFunctions === 'undefined' ? require('../lib/CommonFunction.js') : commonFunctions 10 | 11 | let _own_text = null 12 | 13 | /** 14 | * 查找没有更多了控件是否存在 15 | * 16 | * @param {number} sleepTime 超时时间 17 | */ 18 | const foundNoMoreWidget = function (sleepTime) { 19 | let sleep = sleepTime || _config.timeout_findOne 20 | let noMoreWidgetHeight = 0 21 | 22 | let noMoreWidget = widgetGetOne(_config.no_more_ui_content, sleep, false, true) 23 | if (noMoreWidget) { 24 | let bounds = noMoreWidget.bounds() 25 | debugInfo("找到控件: [" + bounds.left + ", " + bounds.top + ", " + bounds.right + ", " + bounds.bottom + "]") 26 | noMoreWidgetHeight = bounds.bottom - bounds.top 27 | debugInfo('"没有更多了" 当前控件高度:' + noMoreWidgetHeight) 28 | return true 29 | } 30 | return false 31 | } 32 | 33 | /** 34 | * 校验控件是否存在,并打印相应日志 35 | * @param {String} contentVal 控件文本 36 | * @param {String} position 日志内容 当前所在位置是否成功进入 37 | * @param {Number} timeoutSetting 超时时间 默认为_config.timeout_existing 38 | */ 39 | const widgetWaiting = function (contentVal, position, timeoutSetting) { 40 | let waitingSuccess = widgetCheck(contentVal, timeoutSetting) 41 | 42 | if (waitingSuccess) { 43 | debugInfo('成功进入' + position) 44 | return true 45 | } else { 46 | errorInfo('进入' + position + '失败') 47 | return false 48 | } 49 | } 50 | 51 | /** 52 | * 校验控件是否存在 53 | * @param {String} contentVal 控件文本 54 | * @param {Number} timeoutSetting 超时时间 不设置则为_config.timeout_existing 55 | * 超时返回false 56 | */ 57 | const widgetCheck = function (contentVal, timeoutSetting) { 58 | let timeout = timeoutSetting || _config.timeout_existing 59 | let timeoutFlag = true 60 | let countDown = new java.util.concurrent.CountDownLatch(1) 61 | let matchRegex = new RegExp(contentVal) 62 | let descThread = threads.start(function () { 63 | descMatches(matchRegex).waitFor() 64 | let res = descMatches(matchRegex).findOne().desc() 65 | debugInfo('find desc ' + contentVal + " " + res) 66 | timeoutFlag = false 67 | countDown.countDown() 68 | }) 69 | 70 | let textThread = threads.start(function () { 71 | textMatches(matchRegex).waitFor() 72 | let res = textMatches(matchRegex).findOne().text() 73 | debugInfo('find text ' + contentVal + " " + res) 74 | timeoutFlag = false 75 | countDown.countDown() 76 | }) 77 | 78 | let timeoutThread = threads.start(function () { 79 | sleep(timeout) 80 | countDown.countDown() 81 | }) 82 | countDown.await() 83 | descThread.interrupt() 84 | textThread.interrupt() 85 | timeoutThread.interrupt() 86 | return !timeoutFlag 87 | } 88 | 89 | /** 90 | * id检测 91 | * @param {string|RegExp} idRegex 92 | * @param {number} timeoutSetting 93 | */ 94 | const idCheck = function (idRegex, timeoutSetting) { 95 | let timeout = timeoutSetting || _config.timeout_existing 96 | let timeoutFlag = true 97 | let countDown = new java.util.concurrent.CountDownLatch(1) 98 | let idCheckThread = threads.start(function () { 99 | idMatches(idRegex).waitFor() 100 | debugInfo('find id ' + idRegex) 101 | timeoutFlag = false 102 | countDown.countDown() 103 | }) 104 | 105 | let timeoutThread = threads.start(function () { 106 | sleep(timeout) 107 | countDown.countDown() 108 | }) 109 | countDown.await() 110 | idCheckThread.interrupt() 111 | timeoutThread.interrupt() 112 | return !timeoutFlag 113 | } 114 | 115 | /** 116 | * 校验控件是否存在,并打印相应日志 117 | * @param {String} idRegex 控件文本 118 | * @param {String} position 日志内容 当前所在位置是否成功进入 119 | * @param {Number} timeoutSetting 超时时间 默认为_config.timeout_existing 120 | */ 121 | const idWaiting = function (idRegex, position, timeoutSetting) { 122 | let waitingSuccess = idCheck(idRegex, timeoutSetting) 123 | 124 | if (waitingSuccess) { 125 | debugInfo('成功进入' + position) 126 | return true 127 | } else { 128 | errorInfo('进入' + position + '失败') 129 | return false 130 | } 131 | } 132 | 133 | /** 134 | * 校验是否成功进入自己的首页 135 | */ 136 | const homePageWaiting = function () { 137 | if (widgetCheck(_config.friend_home_ui_content, 500)) { 138 | errorInfo('错误位置:当前所在位置为好友首页') 139 | return false; 140 | } 141 | if (idCheck(_config.friend_list_id, 500)) { 142 | errorInfo('错误位置:当前所在位置为好友排行榜') 143 | return false; 144 | } 145 | return widgetWaiting(_config.home_ui_content, '个人首页') 146 | } 147 | 148 | /** 149 | * 校验是否成功进入好友首页 150 | */ 151 | const friendHomeWaiting = function () { 152 | return widgetWaiting(_config.friend_home_ui_content, '好友首页') 153 | } 154 | 155 | /** 156 | * 校验是否成功进入好友排行榜 157 | */ 158 | const friendListWaiting = function () { 159 | return idWaiting(_config.friend_list_id, '好友排行榜') 160 | } 161 | 162 | /** 163 | * 根据内容获取一个对象 164 | * 165 | * @param {string} contentVal 166 | * @param {number} timeout 167 | * @param {boolean} containType 是否带回类型 168 | * @param {boolean} suspendWarning 是否隐藏warning信息 169 | */ 170 | const widgetGetOne = function (contentVal, timeout, containType, suspendWarning) { 171 | let target = null 172 | let isDesc = false 173 | let waitTime = timeout || _config.timeout_findOne 174 | let timeoutFlag = true 175 | let matchRegex = new RegExp(contentVal) 176 | if (textMatches(matchRegex).exists()) { 177 | debugInfo('text ' + contentVal + ' found') 178 | target = textMatches(matchRegex).findOne(waitTime) 179 | timeoutFlag = false 180 | } else if (descMatches(matchRegex).exists()) { 181 | isDesc = true 182 | debugInfo('desc ' + contentVal + ' found') 183 | target = descMatches(matchRegex).findOne(waitTime) 184 | timeoutFlag = false 185 | } else { 186 | debugInfo('none of text or desc found for ' + contentVal) 187 | } 188 | // 当需要带回类型时返回对象 传递target以及是否是desc 189 | if (target && containType) { 190 | let result = { 191 | target: target, 192 | isDesc: isDesc 193 | } 194 | return result 195 | } 196 | if (timeoutFlag) { 197 | if (suspendWarning) { 198 | debugInfo('timeout for finding ' + contentVal) 199 | } else { 200 | warnInfo('timeout for finding ' + contentVal) 201 | } 202 | } 203 | return target 204 | } 205 | 206 | /** 207 | * 根据内容获取所有对象的列表 208 | * 209 | * @param {string} contentVal 210 | * @param {number} timeout 211 | * @param {boolean} containType 是否传递类型 212 | */ 213 | const widgetGetAll = function (contentVal, timeout, containType) { 214 | let target = null 215 | let isDesc = false 216 | let timeoutFlag = true 217 | let countDown = new java.util.concurrent.CountDownLatch(1) 218 | let waitTime = timeout || _config.timeout_findOne 219 | let matchRegex = new RegExp(contentVal) 220 | let findThread = threads.start(function () { 221 | if (textMatches(matchRegex).exists()) { 222 | debugInfo('text ' + contentVal + ' found') 223 | target = textMatches(matchRegex).untilFind() 224 | timeoutFlag = false 225 | } else if (descMatches(matchRegex).exists()) { 226 | isDesc = true 227 | debugInfo('desc ' + contentVal + ' found') 228 | target = descMatches(matchRegex).untilFind() 229 | timeoutFlag = false 230 | } else { 231 | debugInfo('none of text or desc found for ' + contentVal) 232 | } 233 | countDown.countDown() 234 | }) 235 | let timeoutThread = threads.start(function () { 236 | sleep(waitTime) 237 | countDown.countDown() 238 | warnInfo('timeout for finding ' + contentVal) 239 | }) 240 | countDown.await() 241 | findThread.interrupt() 242 | timeoutThread.interrupt() 243 | if (timeoutFlag && !target) { 244 | return null 245 | } else if (target && containType) { 246 | let result = { 247 | target: target, 248 | isDesc: isDesc 249 | } 250 | return result 251 | } 252 | return target 253 | } 254 | 255 | /** 256 | * 加载好友排行榜列表 257 | * @deprecated 新版蚂蚁森林不可用 258 | */ 259 | const loadFriendList = function () { 260 | logInfo('正在展开好友列表请稍等。。。', true) 261 | let start = new Date() 262 | let timeout = true 263 | let countDown = new java.util.concurrent.CountDownLatch(1) 264 | let loadThread = threads.start(function () { 265 | while ((more = idMatches(".*J_rank_list_more.*").findOne(200)) != null) { 266 | more.click() 267 | } 268 | }) 269 | let foundNoMoreThread = threads.start(function () { 270 | widgetCheck(_config.no_more_ui_content, _config.timeoutLoadFriendList || _config.timeout_existing) 271 | timeout = false 272 | countDown.countDown() 273 | }) 274 | let timeoutThread = threads.start(function () { 275 | sleep(_config.timeoutLoadFriendList || _config.timeout_existing) 276 | errorInfo("预加载好友列表超时") 277 | countDown.countDown() 278 | }) 279 | countDown.await() 280 | let end = new Date() 281 | logInfo('好友列表展开' + (timeout ? '超时' : '完成') + ', cost ' + (end - start) + ' ms', true) 282 | // 调试模式时获取信息 283 | if (_config.show_debug_log) { 284 | let friendList = getFriendListParent() 285 | if (friendList && friendList.children) { 286 | debugInfo('好友列表长度:' + friendList.children().length) 287 | } 288 | } 289 | loadThread.interrupt() 290 | foundNoMoreThread.interrupt() 291 | timeoutThread.interrupt() 292 | return timeout 293 | } 294 | 295 | /** 296 | * 获取排行榜好友列表 297 | * @deprecated 新版蚂蚁森林不可用 298 | */ 299 | const getFriendListOld = function () { 300 | let friends_list = null 301 | if (idMatches('J_rank_list_append').exists()) { 302 | debugInfo('newAppendList') 303 | friends_list = idMatches('J_rank_list_append').findOne( 304 | _config.timeout_findOne 305 | ) 306 | } else if (idMatches('J_rank_list').exists()) { 307 | debugInfo('oldList') 308 | friends_list = idMatches('J_rank_list').findOne( 309 | _config.timeout_findOne 310 | ) 311 | } 312 | return friends_list 313 | } 314 | 315 | const getFriendListParent = function getFriendRoot () { 316 | let anyone = null 317 | let regex = /[.\d]+[kgt]+$/ 318 | let countdown = new Countdown() 319 | if (textMatches(regex).exists()) { 320 | anyone = textMatches(regex).findOnce(1) 321 | debugInfo('当前获取到的能量值内容:' + anyone.text()) 322 | } else if (descMatches(regex).exists()) { 323 | debugInfo('当前获取到的能量值内容:' + anyone.desc()) 324 | anyone = descMatches(regex).findOnce(1) 325 | } 326 | countdown.summary('获取能量值控件') 327 | if (anyone) { 328 | try { 329 | return anyone.parent().parent().parent() 330 | } catch (e) { 331 | errorInfo('获取能量值控件失败' + e) 332 | } 333 | } else { 334 | errorInfo('获取能量值控件失败') 335 | } 336 | } 337 | 338 | 339 | function Countdown () { 340 | this.start = new Date().getTime() 341 | this.getCost = function () { 342 | return new Date().getTime() - this.start 343 | } 344 | 345 | this.summary = function (content) { 346 | debugInfo(content + '耗时' + this.getCost() + 'ms') 347 | } 348 | 349 | } 350 | 351 | const getOwntext = function () { 352 | let anyone = null 353 | let regex = /[.\d]+[kgt]+$/ 354 | let countdown = new Countdown() 355 | if (textMatches(regex).exists()) { 356 | anyone = textMatches(regex).findOne(1000) 357 | debugInfo('当前获取到的内容:' + anyone.text()) 358 | } else if (descMatches(regex).exists()) { 359 | debugInfo('当前获取到的内容:' + anyone.desc()) 360 | anyone = descMatches(regex).findOne(1000) 361 | } 362 | countdown.summary('获取能量值控件') 363 | if (anyone) { 364 | try { 365 | let ownElement = anyone.parent().parent().children()[2].children()[0].children()[0] 366 | return ownElement.text() || ownElement.desc() 367 | } catch (e) { 368 | errorInfo(e) 369 | return null 370 | } finally { 371 | countdown.summary('分析自身id') 372 | } 373 | } 374 | 375 | } 376 | 377 | /** 378 | * 获取好友昵称 379 | * 380 | * @param {Object} fri 381 | */ 382 | const getFriendsName = function (fri) { 383 | try { 384 | let nameContainer = fri.child(2).child(0).child(0) 385 | return nameContainer.text() || nameContainer.desc() 386 | } catch (e) { 387 | errorInfo('获取好友名称失败:' + e) 388 | } 389 | } 390 | /** 391 | * 快速下滑 392 | * 用来统计最短时间 393 | */ 394 | const quickScrollDown = function () { 395 | do { 396 | automator.scrollDown(50) 397 | } while ( 398 | !foundNoMoreWidget(50) 399 | ) 400 | } 401 | 402 | /** 403 | * 等待排行榜稳定 404 | * 即不在滑动过程 405 | */ 406 | const waitRankListStable = function () { 407 | let startPoint = new Date() 408 | debugInfo('等待列表稳定') 409 | let compareBottomVal = getJRankSelfBottom() 410 | let size = _config.friendListStableCount || 3 411 | if (size <= 1) { 412 | size = 2 413 | } 414 | let bottomValQueue = _commonFunctions.createQueue(size) 415 | while (_commonFunctions.getQueueDistinctSize(bottomValQueue) > 1) { 416 | compareBottomVal = getJRankSelfBottom() 417 | if (compareBottomVal === undefined && ++invalidCount > 10) { 418 | warnInfo('获取坐标失败次数超过十次') 419 | break 420 | } else { 421 | _commonFunctions.pushQueue(bottomValQueue, size, compareBottomVal) 422 | debugInfo( 423 | '添加参考值:' + compareBottomVal + 424 | '队列重复值数量:' + _commonFunctions.getQueueDistinctSize(bottomValQueue) 425 | ) 426 | } 427 | } 428 | debugInfo('列表已经稳定 等待列表稳定耗时[' + (new Date() - startPoint) + ']ms,不可接受可以调小config.js中的friendListStableCount') 429 | } 430 | 431 | 432 | 433 | /** 434 | * 获取列表中自己的底部高度 435 | */ 436 | const getJRankSelfBottom = function () { 437 | let maxTry = 50 438 | // TODO 当前own_text设为了null,如果设为具体值反而更慢 暂时就这样吧 439 | while (maxTry-- > 0) { 440 | try { 441 | try { 442 | return textMatches(_own_text).findOnce(1).bounds().bottom; 443 | } catch (e) { 444 | try { 445 | return descMatches(_own_text).findOnce(1).bounds().bottom; 446 | } catch (e2) { 447 | // nothing to do here 448 | } 449 | } 450 | } catch (e) { 451 | // nothing to do here 452 | } 453 | } 454 | return null 455 | } 456 | 457 | const getYouCollectEnergy = function () { 458 | let youGet = widgetGetOne('你收取TA') 459 | if (youGet && youGet.parent) { 460 | let youGetParent = youGet.parent() 461 | let childSize = youGetParent.children().length 462 | debugInfo('你收取TA父级控件拥有子控件数量:' + childSize) 463 | let energySum = youGetParent.child(childSize - 1) 464 | if (energySum) { 465 | if (energySum.desc()) { 466 | return energySum.desc().match(/\d+/) 467 | } else if (energySum.text()) { 468 | return energySum.text().match(/\d+/) 469 | } 470 | } 471 | } 472 | return undefined 473 | } 474 | 475 | const getFriendEnergy = function () { 476 | let energyWidget = widgetGetOne(/\d+g/) 477 | if (energyWidget) { 478 | if (energyWidget.desc()) { 479 | return energyWidget.desc().match(/\d+/) 480 | } else { 481 | return energyWidget.text().match(/\d+/) 482 | } 483 | } 484 | return null 485 | } 486 | 487 | 488 | /** 489 | * 给好友浇水 490 | */ 491 | const wateringFriends = function () { 492 | let wateringWidget = widgetGetOne(_config.watering_widget_content) 493 | if (wateringWidget) { 494 | let bounds = wateringWidget.bounds() 495 | automator.click(bounds.centerX(), bounds.centerY()) 496 | debugInfo('found wateringWidget:' + wateringWidget.bounds()) 497 | } else { 498 | errorInfo('未找到浇水按钮') 499 | } 500 | } 501 | 502 | module.exports = { 503 | foundNoMoreWidget: foundNoMoreWidget, 504 | widgetWaiting: widgetWaiting, 505 | widgetCheck: widgetCheck, 506 | idWaiting: idWaiting, 507 | idCheck: idCheck, 508 | homePageWaiting: homePageWaiting, 509 | friendHomeWaiting: friendHomeWaiting, 510 | friendListWaiting: friendListWaiting, 511 | widgetGetOne: widgetGetOne, 512 | widgetGetAll: widgetGetAll, 513 | loadFriendList: loadFriendList, 514 | getFriendListParent: getFriendListParent, 515 | getFriendsName: getFriendsName, 516 | quickScrollDown: quickScrollDown, 517 | waitRankListStable: waitRankListStable, 518 | getJRankSelfBottom: getJRankSelfBottom, 519 | getYouCollectEnergy: getYouCollectEnergy, 520 | getFriendEnergy: getFriendEnergy, 521 | wateringFriends: wateringFriends, 522 | getOwntext: getOwntext 523 | } -------------------------------------------------------------------------------- /lib/autojs-tools.dex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonyJiangWJ/Ant-Forest-autoscript/ae1045091ae65215ad244a5dc884ab7c78ff9369/lib/autojs-tools.dex -------------------------------------------------------------------------------- /lib/mock/mockFunctions.js: -------------------------------------------------------------------------------- 1 | function log(str) { 2 | console.log(str) 3 | } 4 | 5 | function toast(str) { 6 | console.log("toast: " + str) 7 | } 8 | 9 | function sleep(sleep) { 10 | console.log("sleep:" + sleep) 11 | setTimeout(function () { 12 | log("timeout") 13 | }, sleep) 14 | } 15 | 16 | module.exports = { 17 | log, 18 | toast, 19 | sleep 20 | } -------------------------------------------------------------------------------- /lib/scheduler.js: -------------------------------------------------------------------------------- 1 | 2 | let DateCompare = require('./DateCompare.js') 3 | let {formatDate} = require('./DateUtil.js') 4 | let {config} = require('../config.js') 5 | let {commonFunctions} = require('./CommonFunction.js') 6 | const Scheduler = function () { 7 | let sameDay = config.auto_start_same_day 8 | let clockTime = new Date(); 9 | // 如果不是同一天,则将当前时间加24小时 获取第二天的日期 10 | if (!sameDay) { 11 | clockTime = new Date(clockTime.getTime() + 24 * 3600000) 12 | } 13 | // 设置启动时间为第二天的6:40:00 14 | clockTime.setHours(config.auto_start_hours) 15 | clockTime.setMinutes(config.auto_start_minutes) 16 | clockTime.setSeconds(config.auto_start_seconds) 17 | let clockTimeCompare = new DateCompare(clockTime) 18 | logInfo("启动定时:" + formatDate(clockTimeCompare.originDate, "yyyy-MM-dd HH:mm:ss")) 19 | 20 | let nowCompare = new DateCompare(new Date()) 21 | while (nowCompare.compareTo(clockTimeCompare) < 0) { 22 | nowCompare = new DateCompare(new Date()) 23 | logInfo("not now " + formatDate(nowCompare.originDate, "yyyy-MM-dd HH:mm:ss")); 24 | let gap = clockTime.getTime() - nowCompare.getTime(); 25 | if (gap <= 0) { 26 | break; 27 | } 28 | logInfo("当前时间" + formatDate(new Date(nowCompare.getTime()))) 29 | let leftMinutes = (gap / 60000).toFixed(2) 30 | logInfo("剩余时间[" + gap + "]ms [" + leftMinutes + "]分钟 [" + (gap / 3600000).toFixed(2) + "]小时") 31 | logInfo("睡眠" + leftMinutes + "分钟") 32 | commonFunctions.commonDelay(leftMinutes, "睡眠还有[") 33 | } 34 | logInfo("start") 35 | } 36 | 37 | module.exports = { 38 | scheduler: Scheduler 39 | } -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 测试用模块 3 | */ 4 | 5 | 6 | let WidgetUtils = require('./WidgetUtils.js') 7 | let commonFunctions = require('./CommonFunction.js') 8 | let { config } = require('../config.js') 9 | let automator = require('./Automator.js') 10 | let { clearLogFile } = require('./LogUtils.js') 11 | 12 | const getEnergy = function () { 13 | let currentEnergyWidgetContainer = WidgetUtils.widgetGetOne('\\d+g', null, true) 14 | let currentEnergy = undefined 15 | if (currentEnergyWidgetContainer) { 16 | let target = currentEnergyWidgetContainer.target 17 | let isDesc = currentEnergyWidgetContainer.isDesc 18 | if (isDesc) { 19 | currentEnergy = parseInt(target.desc().match(/\d+/)) 20 | } else { 21 | currentEnergy = parseInt(target.text().match(/\d+/)) 22 | } 23 | toastLog(currentEnergy) 24 | } else { 25 | toastLog('cant found energy') 26 | } 27 | } 28 | 29 | const getRankListBottom = function () { 30 | toastLog(idMatches(/.*J_rank_list_self/).findOnce().bounds().bottom) 31 | } 32 | /* 33 | const getFriendEnergy = function () { 34 | let energyWidget = WidgetUtils.widgetGetOne(/\d+g/) 35 | if (energyWidget) { 36 | if (energyWidget.desc()) { 37 | return energyWidget.desc().match(/\d+/) 38 | } else { 39 | return energyWidget.text().match(/\d+/) 40 | } 41 | } 42 | return null 43 | } 44 | */ 45 | 46 | 47 | // commonFunctions.showCollectSummary() 48 | clearLogFile() 49 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: NickHopps 3 | * @Last Modified by: TonyJiangWJ 4 | * @Last Modified time: 2019-12-05 13:36:41 5 | * @Description: 蚂蚁森林自动收能量 6 | */ 7 | let runningQueueDispatcher = require('./lib/RunningQueueDispatcher.js') 8 | let { config } = require('./config.js') 9 | let LogUtils = require('./lib/LogUtils.js') 10 | let { 11 | debugInfo, logInfo, infoLog, warnInfo, errorInfo, clearLogFile, appendLog, removeOldLogFiles 12 | } = LogUtils 13 | let FloatyInstance = require('./lib/FloatyUtil.js') 14 | let commonFunctions = require('./lib/CommonFunction.js') 15 | let unlocker = require('./lib/Unlock.js') 16 | let antForestRunner = require('./core/Ant_forest.js') 17 | let formatDate = require('./lib/DateUtil.js') 18 | let { tryRequestScreenCapture } = require('./lib/TryRequestScreenCapture.js') 19 | logInfo('======校验是否重复运行=======') 20 | // 检查脚本是否重复运行 21 | commonFunctions.checkDuplicateRunning() 22 | // 不管其他脚本是否在运行 清除任务队列 适合只使用蚂蚁森林的用户 23 | if (config.single_script) { 24 | runningQueueDispatcher.clearAll() 25 | } 26 | runningQueueDispatcher.addRunningTask() 27 | /*********************** 28 | * 初始化 29 | ***********************/ 30 | logInfo('======校验无障碍功能======') 31 | // 检查手机是否开启无障碍服务 32 | // 当无障碍经常莫名消失时 可以传递true 强制开启无障碍 33 | // if (!commonFunctions.checkAccessibilityService(true)) { 34 | if (!commonFunctions.checkAccessibilityService()) { 35 | try { 36 | auto.waitFor() 37 | } catch (e) { 38 | warnInfo('auto.waitFor()不可用') 39 | auto() 40 | } 41 | } 42 | logInfo('---前置校验完成;启动系统--->>>>') 43 | if (files.exists('version.json')) { 44 | let content = JSON.parse(files.read('version.json')) 45 | logInfo(['版本信息:{} nodeId:{}', content.version, content.nodeId]) 46 | } else { 47 | logInfo('无法获取脚本版本信息') 48 | } 49 | logInfo('======解锁并校验截图权限======') 50 | try { 51 | unlocker.exec() 52 | } catch (e) { 53 | errorInfo('解锁发生异常, 三分钟后重新开始' + e) 54 | commonFunctions.setUpAutoStart(3) 55 | runningQueueDispatcher.removeRunningTask() 56 | exit() 57 | } 58 | logInfo('解锁成功') 59 | if (config.fuck_miui11) { 60 | commonFunctions.showDialogAndWait() 61 | commonFunctions.launchAutoJs() 62 | } 63 | 64 | // 请求截图权限 65 | let screenPermission = false 66 | let actionSuccess = commonFunctions.waitFor(function () { 67 | if (config.request_capture_permission) { 68 | screenPermission = tryRequestScreenCapture() 69 | } else { 70 | screenPermission = requestScreenCapture(false) 71 | } 72 | }, 15000) 73 | if (!actionSuccess || !screenPermission) { 74 | errorInfo('请求截图失败, 设置6秒后重启') 75 | runningQueueDispatcher.removeRunningTask() 76 | commonFunctions.setUpAutoStart(0.1) 77 | exit() 78 | } else { 79 | logInfo('请求截屏权限成功') 80 | } 81 | // 初始化悬浮窗 82 | if (!FloatyInstance.init()) { 83 | runningQueueDispatcher.removeRunningTask() 84 | // 悬浮窗初始化失败,6秒后重试 85 | commonFunctions.setUpAutoStart(0.1) 86 | } 87 | /************************ 88 | * 主程序 89 | ***********************/ 90 | try { 91 | antForestRunner.exec() 92 | } catch (e) { 93 | commonFunctions.setUpAutoStart(1) 94 | errorInfo('执行异常, 1分钟后重新开始' + e) 95 | } finally { 96 | runningQueueDispatcher.removeRunningTask(true) 97 | } 98 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Custom Ant Forest autoscript", 3 | "main": "main.js", 4 | "ignore": [ 5 | "build" 6 | ], 7 | "packageName": "com.ant-forest-autoscript", 8 | "versionName": "1.1.1.4", 9 | "versionCode": 1 10 | } 11 | -------------------------------------------------------------------------------- /test/BitCheckTest.js: -------------------------------------------------------------------------------- 1 | const CheckBit = function () { 2 | this.BUFFER_LENGTH = Math.ceil((1080 * 10000 + 2160) / 8) 3 | this.BYTE_SIZE = 1 << 3 4 | this.bytes = [] 5 | this.init() 6 | } 7 | 8 | CheckBit.prototype.init = function () { 9 | this.bytes = Array(this.BUFFER_LENGTH + 1).join(0).split('') 10 | } 11 | 12 | CheckBit.prototype.setBit = function (val) { 13 | let idx = ~~(val / this.BYTE_SIZE) 14 | let posi = 1 << (val % this.BYTE_SIZE) 15 | let unset = (this.bytes[idx] & posi) !== posi 16 | this.bytes[idx] = this.bytes[idx] | posi 17 | return unset 18 | } 19 | 20 | CheckBit.prototype.isUnchecked = function (point) { 21 | return this.setBit(point.x * 10000 + point.y) 22 | } 23 | 24 | let checker = new CheckBit() 25 | //checker.init() 26 | let count = 0 27 | let s = new Date().getTime() 28 | for (let i = 0; i < 1080; i++) { 29 | for (let j = 0; j < 2160; j++) { 30 | count++ 31 | checker.isUnchecked({ x: i, y: j }) 32 | } 33 | } 34 | console.log("总数:" + count + " 总耗时:" + (new Date().getTime() - s) + 'ms') -------------------------------------------------------------------------------- /test/ColorDetectTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-12-04 18:09:36 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-04 21:39:34 6 | * @Description: 7 | */ 8 | let ImgBasedFriendListScanner = require('../core/ImgBasedFriendListScanner') 9 | requestScreenCapture(false) 10 | let scanner = new ImgBasedFriendListScanner() 11 | scanner.start() 12 | // var paint = new Paint(); 13 | // //设置画笔为填充,则绘制出来的图形都是实心的 14 | // paint.setStyle(Paint.STYLE.FILL); 15 | // //设置画笔颜色为红色 16 | // paint.setColor(colors.RED); 17 | // //绘制一个从坐标(0, 0)到坐标(100, 100)的正方形 18 | // canvas.drawRect(0, 0, 100, 100, paint); 19 | sleep(2000) 20 | log('再见') -------------------------------------------------------------------------------- /test/FloatyTest.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-12-02 19:27:26 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-02 20:29:47 6 | * @Description: 7 | */ 8 | // let { floatyUtil } = require('../lib/FloatyUtil.js') 9 | let FloatyInstance = require('../lib/FloatyUtil.js') 10 | let { tester } = require('./FloatyTest2.js') 11 | 12 | FloatyInstance.setFloatyInfo({x: 200, y: 1500}, 'Hello. World!') 13 | sleep(1000) 14 | FloatyInstance.setFloatyTextColor('#ff0000') 15 | FloatyInstance.setFloatyInfo({x: 200, y: 1500}, 'Ready For Test!') 16 | sleep(2000) 17 | tester.run() 18 | setTimeout(function () { 19 | exit() 20 | }, 10000) -------------------------------------------------------------------------------- /test/FloatyTest2.js: -------------------------------------------------------------------------------- 1 | let _FloatyInstance = typeof FloatyInstance === 'undefined' ? ( 2 | function () { 3 | return require('../lib/FloatyUtil.js') 4 | })() : FloatyInstance 5 | 6 | module.exports = { 7 | tester: { 8 | run: function () { 9 | _FloatyInstance.setFloatyInfo({ x: 300, y: 1600 }, 'Hello? Anyone here?') 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /test/FloatyTest3.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-12-02 19:27:26 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-02 20:35:07 6 | * @Description: 7 | */ 8 | // let { floatyUtil } = require('../lib/FloatyUtil.js') 9 | let FloatyInstance = require('../lib/FloatyUtil.js') 10 | let FloatyInstance2 = require('../lib/FloatyUtil.js') 11 | 12 | FloatyInstance.setFloatyInfo({x: 200, y: 1500}, 'Hello. World!') 13 | sleep(1000) 14 | FloatyInstance.setFloatyTextColor('#ff0000') 15 | FloatyInstance.setFloatyInfo({x: 200, y: 1500}, 'Ready For Test!') 16 | sleep(1000) 17 | // floaty.closeAll() 18 | // FloatyInstance.close() 19 | sleep(2000) 20 | FloatyInstance2.setFloatyTextColor('#00ff00') 21 | FloatyInstance2.setFloatyInfo({x: 200, y: 1500}, 'Test Instance2!') 22 | setTimeout(function () { 23 | exit() 24 | }, 10000) -------------------------------------------------------------------------------- /test/TestBit.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-12-06 11:33:42 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-06 15:19:52 6 | * @Description: 7 | */ 8 | runtime.loadDex('../lib/autojs-tools.dex') 9 | importClass(com.tony.BitCheck) 10 | importClass(java.util.HashMap) 11 | let checker = new BitCheck(1080 * 10000 + 2160) 12 | let count = 0 13 | let start 14 | 15 | 16 | count = 0 17 | start = new Date().getTime() 18 | for (let x = 0; x < 1080; x++) { 19 | for (let y = 0; y < 2160; y++) { 20 | count++ 21 | if (!checker.isUnchecked(x * 10000 + y)) { 22 | console.log('[' + x + ',' + y + '] 已经校验过') 23 | } 24 | } 25 | } 26 | console.log('位校验完成 总数:' + count + ' 耗时:' + (new Date().getTime() - start) + 'ms') 27 | 28 | let hashCheck = {} 29 | count = 0 30 | start = new Date().getTime() 31 | for (let x = 0; x < 1080; x++) { 32 | for (let y = 0; y < 2160; y++) { 33 | count++ 34 | let key = x * 10000 + y 35 | if (!hashCheck[key]) { 36 | hashCheck[key] = true 37 | } 38 | } 39 | } 40 | console.log('hash校验完成 总数:' + count + ' 耗时:' + (new Date().getTime() - start) + 'ms') 41 | 42 | 43 | let hashMap = new HashMap(2160 * 1080) 44 | count = 0 45 | start = new Date().getTime() 46 | for (let x = 0; x < 1080; x++) { 47 | for (let y = 0; y < 2160; y++) { 48 | count++ 49 | let key = x * 10000 + y 50 | if (!hashMap.get(key)) { 51 | hashMap.put(key, true) 52 | } 53 | } 54 | } 55 | console.log('hashMap校验完成 总数:' + count + ' 耗时:' + (new Date().getTime() - start) + 'ms') 56 | 57 | 58 | -------------------------------------------------------------------------------- /test/testScrollUpAndDown.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-12-03 19:33:46 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-03 19:34:24 6 | * @Description: 7 | */ 8 | let automator = require('../lib/Automator.js') 9 | automator.scrollUpAndDown() -------------------------------------------------------------------------------- /unit/FriendListGet.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-11-30 11:34:59 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-02 23:27:10 6 | * @Description: 测试功能用 7 | */ 8 | let config = { 9 | friend_list_ui_content: '(周|总)排行榜', 10 | help_friend: true 11 | } 12 | 13 | console.show() 14 | 15 | checkList() 16 | 17 | 18 | function checkList () { 19 | let countdown = new Countdown() 20 | // let own = getOwntext() 21 | // console.log('我自己的ID: ' + own + ' 耗时:' + countdown.getCost()) 22 | 23 | countdown = new Countdown() 24 | let friendsList = getFriendRoot() 25 | countdown.summary('获取好友列表Root_') 26 | let isValidLength = whetherFriendListValidLength(friendsList) 27 | if (isValidLength) { 28 | console.log('好友列表初步判断有效 长度为:' + isValidLength) 29 | 30 | let validList = getValidChildList(friendsList) 31 | if (validList && validList.length > 0) { 32 | console.log('好友列表确实有效:') 33 | } else { 34 | console.log('好友列表最终判断无效:') 35 | } 36 | for (let i = 0; i < (isValidLength > 10 ? 10 : isValidLength); i++) { 37 | let target = friendsList.children()[i] 38 | if (target && target.children()) { 39 | if (target.children().length > 3) { 40 | console.log('idx:' + i + ' 被检测的text: ' + target.child(3).child(0).text() + ' desc:' + target.child(3).child(0).desc()) 41 | } else { 42 | console.log('idx:' + i + 'children长度不符合:' + target.children().length) 43 | } 44 | } else { 45 | console.log('idx:' + i + '无法获取children') 46 | } 47 | } 48 | } else { 49 | console.log('好友列表初步判断无效 ' + isValidLength) 50 | } 51 | } 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | // ............... 63 | 64 | function getJRankSelfBottom () { 65 | let maxTry = 50 66 | while (maxTry-- > 0) { 67 | try { 68 | return textMatches(_own_text).findOnce(1).bounds().bottom; 69 | } catch (e) { 70 | try { 71 | return descMatches(_own_text).findOnce(1).bounds().bottom; 72 | } catch (e2) { 73 | // nothing to do here 74 | } 75 | } 76 | } 77 | return null 78 | } 79 | 80 | function getH5RankSelfBottom () { 81 | let maxTry = 50 82 | while (maxTry-- > 0) { 83 | try { 84 | return idMatches(/.*h5_h_divider/).findOnce().bounds().bottom; 85 | } catch (e) { 86 | // nothing to do here 87 | } 88 | } 89 | return null 90 | } 91 | 92 | function getValidChildList (friends_list_parent) { 93 | return friends_list_parent.children().filter((fri) => { 94 | if (!fri) { 95 | return false 96 | } 97 | if (fri.childCount() >= 4) { 98 | let text = fri.child(3).child(0).text() 99 | let desc = fri.child(3).child(0).desc() 100 | let content = text || desc 101 | let regex = /.*[tg]|(证书)$/ 102 | let checkResult = regex.test(content) 103 | if (!checkResult) { 104 | console.log(getFriendsName(fri) + '判断无效 控件的text:' + text + ' desc:' + desc) 105 | } 106 | return checkResult 107 | } else { 108 | let name = getFriendsName(fri) 109 | console.log(name + "不是有效的列表") 110 | } 111 | return false 112 | }) 113 | } 114 | 115 | function whetherFriendListValidLength (friends_list) { 116 | return (friends_list && friends_list.children()) ? friends_list.children().length : undefined 117 | } 118 | function getFriendsName (fri) { 119 | try { 120 | let nameContainer = fri.child(2).child(0).child(0) 121 | return nameContainer.text() || nameContainer.desc() 122 | } catch (e) { 123 | log(e) 124 | } 125 | } 126 | /** 127 | * 格式化参数 128 | * @param {string|array} originContent 需要格式化参数数组或者不需要格式化的字符串 129 | */ 130 | function convertObjectContent (originContent) { 131 | if (typeof originContent === 'string') { 132 | return originContent 133 | } else if (Array.isArray(originContent)) { 134 | // let [marker, ...args] = originContent 135 | let marker = originContent[0] 136 | let args = originContent.slice(1) 137 | let regex = /(\{\})/g 138 | let matchResult = marker.match(regex) 139 | if (matchResult && args && matchResult.length > 0 && matchResult.length === args.length) { 140 | args.forEach((item, idx) => { 141 | marker = marker.replace('{}', item) 142 | }) 143 | return marker 144 | } else if (matchResult === null) { 145 | return marker 146 | } 147 | } 148 | console.error('参数不匹配[' + originContent + ']') 149 | return originContent 150 | } 151 | 152 | function debugInfo (content, isToast) { 153 | let c = convertObjectContent(content) 154 | console.verbose(c) 155 | } 156 | 157 | function isObtainable (obj, screen) { 158 | let container = { 159 | fri: obj, 160 | isHelp: false, 161 | name: getFriendsName(obj), 162 | canDo: false 163 | } 164 | 165 | let len = obj.childCount() 166 | if (len < 5) { 167 | return container 168 | } 169 | // 分析目标控件的索引 170 | let targetIdx = 4 171 | if (!config.is_cycle) { 172 | let countDO = obj.child(targetIdx) 173 | if (countDO.childCount() > 0) { 174 | let cc = countDO.child(0) 175 | debugInfo(['获取[{}] 倒计时数据[{}] ', container.name, (cc.desc() ? cc.desc() : cc.text())]) 176 | let num = null 177 | if (cc.desc()) { 178 | num = parseInt(cc.desc().match(/\d+/)) 179 | } 180 | if (!num && cc.text()) { 181 | num = parseInt(cc.text().match(/\d+/)) 182 | } 183 | if (isFinite(num)) { 184 | debugInfo([ 185 | '记录[{}] 倒计时[{}]分 time[{}]', 186 | container.name, num, new Date().getTime() 187 | ]) 188 | if (config.cutAndSaveCountdown) { 189 | let countdownInfo = { 190 | name: container.name, 191 | x: obj.bounds().left, 192 | y: obj.bounds().top, 193 | h: obj.bounds().height(), 194 | w: obj.bounds().width(), 195 | countdown: num 196 | } 197 | cutAndSaveImage(screen, countdownInfo) 198 | } 199 | container.countdown = { 200 | count: num, 201 | stamp: new Date().getTime() 202 | } 203 | return container 204 | } 205 | 206 | } 207 | } 208 | let o_x = obj.child(targetIdx).bounds().left, 209 | o_y = obj.bounds().top, 210 | o_w = 5, 211 | o_h = obj.bounds().height() - 10, 212 | threshold = config.color_offset 213 | if (o_h > 50) { 214 | o_h = 50 215 | } 216 | if (o_h > 0) { 217 | try { 218 | if ( 219 | // 是否可收取 220 | images.findColor(screen, config.can_collect_color || '#1da06a', { 221 | region: [o_x, o_y, o_w, o_h], 222 | threshold: threshold 223 | }) 224 | ) { 225 | container.canDo = true 226 | } else if ( 227 | config.help_friend && 228 | // 是否可帮收取 229 | images.findColor(screen, config.can_help_color || '#f99236', { 230 | region: [o_x, o_y, o_w, o_h], 231 | threshold: threshold 232 | }) 233 | ) { 234 | container.canDo = true 235 | container.isHelp = true 236 | } 237 | if (container.canDo || container.isHelp) { 238 | // warnInfo(['剪切图片识别区域「x:{} y:{} w:{} h:{}」base64:[\ndata:image/png;base64, {}\n]', o_x, o_y, o_w, o_h, images.toBase64(images.clip(screen, o_x, o_y, o_w, o_h))]) 239 | // warnInfo(['原始图片信息 base64[\ndata:image/png;base64, {}\n]', images.toBase64(screen, 'png', 5)]) 240 | } 241 | } catch (e) { 242 | // errorInfo(['图片分析失败{} base64:[data:image/png;base64, {}]', e, images.toBase64(screen, 'png', 50)]) 243 | debugInfo(['图片分析失败{} imgsize:[w:{}, h:{}]', e, screen.getWidth(), screen.getHeight()]) 244 | } 245 | } 246 | return container 247 | } 248 | 249 | 250 | function friendListWaiting () { 251 | return widgetWaiting(config.friend_list_ui_content, '好友排行榜') 252 | } 253 | 254 | function widgetWaiting (contentVal, position, timeoutSetting) { 255 | let waitingSuccess = widgetCheck(contentVal, timeoutSetting) 256 | 257 | if (waitingSuccess) { 258 | log('成功进入' + position) 259 | return true 260 | } else { 261 | log('进入' + position + '失败') 262 | return false 263 | } 264 | } 265 | 266 | 267 | /** 268 | * 校验控件是否存在 269 | * @param {String} contentVal 控件文本 270 | * @param {Number} timeoutSetting 超时时间 不设置则为config.timeout_existing 271 | * 超时返回false 272 | */ 273 | function widgetCheck (contentVal, timeoutSetting) { 274 | let timeout = timeoutSetting || 1000 275 | let timeoutFlag = true 276 | let countDown = new java.util.concurrent.CountDownLatch(1) 277 | let descThread = threads.start(function () { 278 | descMatches(contentVal).waitFor() 279 | let res = descMatches(contentVal).findOne().desc() 280 | log('find desc ' + contentVal + " " + res) 281 | timeoutFlag = false 282 | countDown.countDown() 283 | }) 284 | 285 | let textThread = threads.start(function () { 286 | textMatches(contentVal).waitFor() 287 | let res = textMatches(contentVal).findOne().text() 288 | log('find text ' + contentVal + " " + res) 289 | timeoutFlag = false 290 | countDown.countDown() 291 | }) 292 | 293 | let timeoutThread = threads.start(function () { 294 | sleep(timeout) 295 | countDown.countDown() 296 | }) 297 | countDown.await() 298 | descThread.interrupt() 299 | textThread.interrupt() 300 | timeoutThread.interrupt() 301 | return !timeoutFlag 302 | } 303 | 304 | 305 | function getFriendsList (ownText) { 306 | if (ownText) { 307 | // log('我自己:' + ownText) 308 | let allSelf = textMatches(new RegExp('^' + ownText + '$')).find() 309 | if (allSelf && allSelf.length >= 2) { 310 | try { 311 | let rootParent = allSelf[1].parent().parent().parent().parent() 312 | // log('列表总长度:' + rootParent.children().length) 313 | return rootParent 314 | } catch (e) { 315 | log(e) 316 | } finally { 317 | // countdown.summary('获取好友列表根节点') 318 | } 319 | } 320 | } 321 | } 322 | 323 | function getFriendsList1 (ownText) { 324 | if (ownText) { 325 | log('我自己:' + ownText) 326 | let self = textMatches(new RegExp('^' + ownText + '$')).findOnce(1) 327 | if (self) { 328 | try { 329 | let rootParent = self.parent().parent().parent().parent() 330 | log('列表总长度:' + rootParent.children().length) 331 | return rootParent 332 | } catch (e) { 333 | log(e) 334 | } finally { 335 | countdown.summary('获取好友列表根节点') 336 | } 337 | } 338 | } 339 | } 340 | 341 | 342 | function getFriendsList2 (ownText) { 343 | let countdown = new Countdown() 344 | if (ownText) { 345 | log('我自己:' + ownText) 346 | let regex = new RegExp('^' + ownText + '$') 347 | let self = descMatches(regex).findOnce(1) || textMatches(regex).findOnce(1) 348 | if (self) { 349 | try { 350 | let rootParent = self.parent().parent().parent().parent() 351 | iteratorAllText(rootParent) 352 | log('列表总长度:' + rootParent.children().length) 353 | return rootParent 354 | } catch (e) { 355 | log(e) 356 | } finally { 357 | countdown.summary('获取好友列表根节点') 358 | } 359 | } else { 360 | log('获取我自己的控件失败:' + ownText) 361 | } 362 | } 363 | } 364 | 365 | function Countdown () { 366 | this.start = new Date().getTime() 367 | this.getCost = function () { 368 | return new Date().getTime() - this.start 369 | } 370 | 371 | this.summary = function (content) { 372 | console.log(content + '耗时' + this.getCost() + 'ms') 373 | } 374 | 375 | } 376 | 377 | function getFriendRoot() { 378 | let anyone = null 379 | let regex = /[.\d]+[kgt]+$/ 380 | let countdown = new Countdown() 381 | if (textMatches(regex).exists()) { 382 | anyone = textMatches(regex).findOnce(1) 383 | log('当前获取到的内容:' + anyone.text()) 384 | } else if (descMatches(regex).exists()) { 385 | log('当前获取到的内容:' + anyone.desc()) 386 | anyone = descMatches(regex).findOnce(1) 387 | } 388 | countdown.summary('获取能量值控件') 389 | if (anyone) { 390 | let root = anyone.parent().parent().parent() 391 | return root 392 | } else { 393 | log('获取能量值控件失败') 394 | } 395 | } 396 | 397 | function getOwntext () { 398 | let anyone = null 399 | let regex = /[.\d]+[kgt]+$/ 400 | let countdown = new Countdown() 401 | if (textMatches(regex).exists()) { 402 | anyone = textMatches(regex).findOnce(1000) 403 | log('当前获取到的内容:' + anyone.text()) 404 | } else if (descMatches(regex).exists()) { 405 | log('当前获取到的内容:' + anyone.desc()) 406 | anyone = descMatches(regex).findOne(1000) 407 | } else { 408 | log('获取能量值控件失败') 409 | } 410 | countdown.summary('获取能量值控件') 411 | if (anyone) { 412 | try { 413 | let ownElement = anyone.parent().parent().children()[2].children()[0].children()[0] 414 | let text = ownElement.text() 415 | let desc = ownElement.desc() 416 | console.log('个人id控件文本 text:' + text + ' desc:' + desc) 417 | return text || desc 418 | } catch (e) { 419 | log('获取个人id文本失败' + e) 420 | return null 421 | } finally { 422 | countdown.summary('分析自身id') 423 | } 424 | } 425 | 426 | } 427 | 428 | 429 | function iteratorAllText (element, parent) { 430 | if (element) { 431 | let content = element.desc() || element.text() 432 | log(parent + '>' + content) 433 | if (element.children()) { 434 | element.children().forEach((child, idx) => { 435 | iteratorAllText(child, parent + '>' + content + ':' + idx) 436 | }) 437 | } 438 | } 439 | } -------------------------------------------------------------------------------- /unit/queueTest/Task1.js: -------------------------------------------------------------------------------- 1 | let runningQueueDispatcher = require('../../lib/RunningQueueDispatcher.js') 2 | let commonFunctions = require('../../lib/CommonFunction.js') 3 | 4 | runningQueueDispatcher.addRunningTask() 5 | let count = 15 6 | while (count-- > 0) { 7 | let content = 'Task1 Running count:' + count 8 | log(content) 9 | commonFunctions.showMiniFloaty(content, 700 - count * 10, 700 - count * 10, '#00FF00') 10 | sleep(1000) 11 | } 12 | runningQueueDispatcher.removeRunningTask() -------------------------------------------------------------------------------- /unit/queueTest/Task2.js: -------------------------------------------------------------------------------- 1 | let runningQueueDispatcher = require('../../lib/RunningQueueDispatcher.js') 2 | let commonFunctions = require('../../lib/CommonFunction.js') 3 | 4 | runningQueueDispatcher.addRunningTask() 5 | let count = 15 6 | while (count-- > 0) { 7 | let content = 'Task2 Running count:' + count 8 | log(content) 9 | commonFunctions.showMiniFloaty(content, 500 - count * 10, 600 - count * 10, '#ff0000') 10 | sleep(1000) 11 | } 12 | runningQueueDispatcher.removeRunningTask() -------------------------------------------------------------------------------- /unit/queueTest/Task3.js: -------------------------------------------------------------------------------- 1 | let runningQueueDispatcher = require('../../lib/RunningQueueDispatcher.js') 2 | let commonFunctions = require('../../lib/CommonFunction.js') 3 | 4 | runningQueueDispatcher.addRunningTask() 5 | let count = 15 6 | while (count-- > 0) { 7 | let content = 'Task3 Running count:' + count 8 | log(content) 9 | commonFunctions.showMiniFloaty(content, 400 - count * 10, 500 - count * 10, '#0000ff') 10 | sleep(1000) 11 | } 12 | runningQueueDispatcher.removeRunningTask() -------------------------------------------------------------------------------- /unit/queueTest/TaskTestManager.js: -------------------------------------------------------------------------------- 1 | let runningQueueDispatcher = require('../../lib/RunningQueueDispatcher.js') 2 | let pwd = files.cwd() 3 | 4 | let task1 = pwd + '/Task1.js' 5 | let task2 = pwd + '/Task2.js' 6 | let task3 = pwd + '/Task3.js' 7 | 8 | runningQueueDispatcher.setUpAutoStart(task1, 1) 9 | runningQueueDispatcher.setUpAutoStart(task2, 1) 10 | runningQueueDispatcher.setUpAutoStart(task3, 1) 11 | 12 | // 继续重复执行 13 | runningQueueDispatcher.setUpAutoStart(task1, 1) 14 | runningQueueDispatcher.setUpAutoStart(task2, 1) 15 | runningQueueDispatcher.setUpAutoStart(task3, 1) -------------------------------------------------------------------------------- /unit/功能测试-任务队列测试.js: -------------------------------------------------------------------------------- 1 | let runningQueueDispatcher = require('../lib/RunningQueueDispatcher.js') 2 | 3 | runningQueueDispatcher.showDispatchStatus() 4 | runningQueueDispatcher.addRunningTask() 5 | sleep(500) 6 | runningQueueDispatcher.showDispatchStatus() 7 | runningQueueDispatcher.addRunningTask() 8 | sleep(500) 9 | runningQueueDispatcher.showDispatchStatus() 10 | runningQueueDispatcher.removeRunningTask() 11 | sleep(500) 12 | runningQueueDispatcher.showDispatchStatus() 13 | // runningQueueDispatcher.clearAll() 14 | 15 | -------------------------------------------------------------------------------- /unit/功能测试-查看运行队列.js: -------------------------------------------------------------------------------- 1 | let runningQueueDispatcher = require('../lib/RunningQueueDispatcher.js') 2 | 3 | runningQueueDispatcher.showDispatchStatus() 4 | 5 | -------------------------------------------------------------------------------- /unit/功能测试-清空任务队列.js: -------------------------------------------------------------------------------- 1 | let runningQueueDispatcher = require('../lib/RunningQueueDispatcher.js') 2 | 3 | runningQueueDispatcher.showDispatchStatus() 4 | runningQueueDispatcher.clearAll() 5 | 6 | -------------------------------------------------------------------------------- /unit/功能测试.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: TonyJiangWJ 3 | * @Date: 2019-11-09 11:14:45 4 | * @Last Modified by: TonyJiangWJ 5 | * @Last Modified time: 2019-12-04 09:25:09 6 | * @Description: 7 | */ 8 | let WidgetUtils = require('../lib/WidgetUtils.js') 9 | let commonFunctions = require('../lib/CommonFunction.js') 10 | let _config = typeof config === 'undefined' ? require('../config.js').config : config 11 | let innerAutomator = typeof automator === 'undefined' ? require('../lib/Automator.js') : automator 12 | let Timers = require('../lib/Timers.js')(runtime, this) 13 | let { 14 | debugInfo, logInfo, infoLog, warnInfo, errorInfo, clearLogFile, removeOldLogFiles 15 | } = require('../lib/LogUtils.js') 16 | 17 | debugInfo('debug') 18 | logInfo('log') 19 | infoLog('info') 20 | warnInfo('warn') 21 | errorInfo('error') 22 | // commonFunctions.showDialogAndWait() 23 | // commonFunctions.showAllAutoTimedTask() 24 | 25 | // commonFunctions.minimize() 26 | // innerAutomator.clickBack() 27 | console.show() 28 | toastLog(WidgetUtils.idCheck('.*react-content.*')) -------------------------------------------------------------------------------- /unit/展示今日收集能量信息.js: -------------------------------------------------------------------------------- 1 | 2 | let commonFunctions = require('../lib/CommonFunction.js') 3 | 4 | log('=============总收集排序==============') 5 | //commonFunctions.showCollectSummary() 6 | log('=============今日收集排序==============') 7 | commonFunctions.showCollectSummary('todayCollect') 8 | //log('=============帮助排序==============') 9 | //commonFunctions.showCollectSummary('todayHelp') -------------------------------------------------------------------------------- /unit/校验保护罩.js: -------------------------------------------------------------------------------- 1 | let WidgetUtils = require('../lib/WidgetUtils.js') 2 | let commonFunctions = require('../lib/CommonFunction.js') 3 | let { config } = require('../config.js') 4 | let automator = require('../lib/Automator.js') 5 | let Timers = require('../lib/Timers.js')(runtime, this) 6 | let { 7 | debugInfo, logInfo, infoLog, warnInfo, errorInfo 8 | } = require('../lib/LogUtils.js') 9 | let { 10 | formatDate 11 | } = require('../lib/DateUtil.js') 12 | 13 | const protectInfoDetect = function () { 14 | let usingInfo = WidgetUtils.widgetGetOne('使用了保护罩', 50, true) 15 | if (usingInfo !== null) { 16 | let target = usingInfo.target 17 | warnInfo(['found using protect info, bounds:{}', target.bounds()], true) 18 | let parent = target.parent().parent() 19 | let targetRow = parent.row() 20 | let time = parent.child(1).text() 21 | if (!time) { 22 | time = parent.child(1).desc() 23 | } 24 | let isToday = true 25 | let yesterday = WidgetUtils.widgetGetOne('昨天', 50, true) 26 | let yesterdayRow = null 27 | if (yesterday !== null) { 28 | yesterdayRow = yesterday.target.row() 29 | // warnInfo(yesterday.target.indexInParent(), true) 30 | isToday = yesterdayRow > targetRow 31 | } 32 | // if (!isToday) { 33 | // 获取前天的日期 34 | let dateBeforeYesterday = formatDate(new Date(new Date().getTime() - 3600 * 24 * 1000 * 2), 'MM-dd') 35 | let dayBeforeYesterday = WidgetUtils.widgetGetOne(dateBeforeYesterday, 50, true) 36 | if (dayBeforeYesterday !== null) { 37 | let dayBeforeYesterdayRow = dayBeforeYesterday.target.row() 38 | if (dayBeforeYesterdayRow < targetRow) { 39 | debugInfo('能量罩使用时间已超时,前天之前的数据') 40 | return false 41 | } else { 42 | infoLog(['前天row:{}', dayBeforeYesterdayRow]) 43 | } 44 | } 45 | // } 46 | warnInfo(['using time:{}-{} bottoms: y[{}] t[{}]', isToday ? '今天' : '昨天', time, yesterdayRow, targetRow], true) 47 | return true 48 | } else { 49 | warnInfo('not found using protect info', true) 50 | } 51 | return false 52 | } 53 | 54 | protectInfoDetect() -------------------------------------------------------------------------------- /unit/浇水三次.js: -------------------------------------------------------------------------------- 1 | let WidgetUtils = require('../lib/WidgetUtils.js') 2 | let commonFunctions = require('../lib/CommonFunction.js') 3 | let { config } = require('../config.js') 4 | let automator = require('../lib/Automator.js') 5 | let { 6 | debugInfo, logInfo, infoLog, warnInfo, errorInfo 7 | } = require('../lib/LogUtils.js') 8 | let count = 0 9 | while (count++ < 5) { 10 | WidgetUtils.wateringFriends() 11 | sleep(1500) 12 | } 13 | toast('done') -------------------------------------------------------------------------------- /unit/能量收集统计.js: -------------------------------------------------------------------------------- 1 | let WidgetUtils = require('../lib/WidgetUtils.js') 2 | let commonFunctions = require('../lib/CommonFunction.js') 3 | let { config } = require('../config.js') 4 | let automator = require('../lib/Automator.js') 5 | let { 6 | debugInfo, logInfo, infoLog, warnInfo, errorInfo 7 | } = require('../lib/LogUtils.js') 8 | const PACKAGE_NAME = 'com.eg.android.AlipayGphone' 9 | const START_DATA = 'alipays://platformapi/startapp?appId=60000002' 10 | 11 | // 进入蚂蚁森林主页 12 | const startApp = function (packageName, startData, action) { 13 | logInfo('启动应用' + packageName) 14 | app.startActivity({ 15 | action: action || 'VIEW', 16 | data: startData, 17 | packageName: packageName 18 | }) 19 | } 20 | 21 | const checkFriends = function () { 22 | WidgetUtils.homePageWaiting() 23 | automator.enterFriendList() 24 | WidgetUtils.friendListWaiting() 25 | let lastCheckFriend = -1 26 | let friendListLength = -2 27 | debugInfo('加载好友列表') 28 | WidgetUtils.loadFriendList() 29 | if (!WidgetUtils.friendListWaiting()) { 30 | errorInfo('崩了 当前不在好友列表 重新开始') 31 | return false 32 | } 33 | commonFunctions.addOpenPlacehold("<<<<>>>>") 34 | let queue = commonFunctions.createQueue(3) 35 | do { 36 | sleep(50) 37 | WidgetUtils.waitRankListStable() 38 | let screen = captureScreen() 39 | debugInfo('获取好友列表') 40 | let friends_list = WidgetUtils.getFriendListParent() 41 | debugInfo('判断好友信息') 42 | if (friends_list && friends_list.children) { 43 | friendListLength = friends_list.children().length 44 | debugInfo( 45 | '读取好友列表完成 列表长度:' + friendListLength 46 | ) 47 | friends_list.children().forEach(function (fri, idx) { 48 | if (fri.visibleToUser()) { 49 | if (fri.childCount() >= 3) { 50 | let bounds = fri.bounds() 51 | let fh = bounds.bottom - bounds.top 52 | if (fh > 10 && idx >= lastCheckFriend) { 53 | // 进入好友首页并记录数据 54 | collectTargetFriend(fri) 55 | // 记录最后一个校验的下标索引, 也就是最后出现在视野中的 56 | lastCheckFriend = idx + 1 57 | } else { 58 | debugInfo('不在视野范围' + idx + ' name:' + WidgetUtils.getFriendsName(fri)) 59 | } 60 | } else { 61 | debugInfo('不符合好友列表条件 childCount:' + fri.childCount() + ' index:' + idx) 62 | } 63 | } 64 | }) 65 | } else { 66 | logInfo('好友列表不存在') 67 | } 68 | if (!WidgetUtils.friendListWaiting()) { 69 | errorInfo('崩了 当前不在好友列表 重新开始') 70 | return false 71 | } 72 | debugInfo('收集数据完成 last:' + lastCheckFriend + ',下滑进入下一页') 73 | automator.scrollDown(50) 74 | debugInfo('进入下一页') 75 | commonFunctions.pushQueue(queue, 3, lastCheckFriend) 76 | } while ( 77 | lastCheckFriend < friendListLength && commonFunctions.getQueueDistinctSize(queue) > 1 78 | ) 79 | commonFunctions.addClosePlacehold(">>>><<<<") 80 | logInfo('全部好友数据收集完成, last:' + lastCheckFriend + ' length:' + friendListLength) 81 | } 82 | 83 | 84 | const collectTargetFriend = function (fri) { 85 | let startPoint = new Date() 86 | let name = WidgetUtils.getFriendsName(fri) 87 | if (name == 'TonyJiang') { 88 | logInfo('跳过自身', true) 89 | return 90 | } 91 | if (fri.child(2).desc() === '邀请' || fri.child(2).text() === '邀请') { 92 | logInfo('需要邀请好友:' + name) 93 | return 94 | } 95 | let obj = { 96 | name: name, 97 | target: fri.bounds() 98 | } 99 | //automator.click(obj.target.centerX(), obj.target.centerY()) 100 | debugInfo('等待进入好友主页:' + name) 101 | let restartLoop = false 102 | let count = 1 103 | automator.click(obj.target.centerX(), obj.target.centerY()) 104 | ///sleep(1000) 105 | while (!WidgetUtils.friendHomeWaiting()) { 106 | warnInfo( 107 | '未能进入' + name + '主页,尝试再次进入 count:' + count++ 108 | ) 109 | automator.click(obj.target.centerX(), obj.target.centerY()) 110 | sleep(1000) 111 | if (count > 5) { 112 | warnInfo('重试超过5次,取消操作') 113 | restartLoop = true 114 | break 115 | } 116 | } 117 | if (restartLoop) { 118 | errorInfo('页面流程出错,重新开始') 119 | return false 120 | } 121 | 122 | if (WidgetUtils.widgetCheck('加为好友|返回我的森林', 200)) { 123 | errorInfo(name + ' TA已经把你删啦') 124 | // return 125 | } else { 126 | debugInfo('准备开始统计') 127 | let preGot 128 | let preE 129 | try { 130 | preGot = WidgetUtils.getYouCollectEnergy() || 0 131 | preE = WidgetUtils.getFriendEnergy() 132 | commonFunctions.recordFriendCollectInfo({ 133 | friendName: name, 134 | friendEnergy: preE, 135 | postCollect: preGot, 136 | preCollect: preGot, 137 | helpCollect: 0 138 | }) 139 | } catch (e) { 140 | errorInfo("[" + obj.name + "]获取收集能量异常" + e) 141 | } 142 | } 143 | 144 | automator.back() 145 | debugInfo('好友能量数据收集完毕, 回到好友排行榜') 146 | logInfo('统计用时:' + (new Date() - startPoint) + 'ms') 147 | let returnCount = 0 148 | while (!WidgetUtils.friendListWaiting()) { 149 | sleep(1000) 150 | if (returnCount++ === 2) { 151 | // 等待两秒后再次触发 152 | automator.back() 153 | } 154 | if (returnCount > 5) { 155 | errorInfo('返回好友排行榜失败,重新开始') 156 | return false 157 | } 158 | } 159 | } 160 | // 检查手机是否开启无障碍服务 161 | try { 162 | auto.waitFor() 163 | } catch (e) { 164 | warnInfo('auto.waitFor()不可用') 165 | auto() 166 | } 167 | // 请求截图权限 168 | if (!requestScreenCapture()) { 169 | errorInfo('请求截图失败') 170 | exit() 171 | } else { 172 | logInfo('请求截图权限成功') 173 | } 174 | startApp(PACKAGE_NAME, START_DATA) 175 | checkFriends() 176 | commonFunctions.showCollectSummary() 177 | toastLog('done') 178 | automator.clickClose() 179 | home() -------------------------------------------------------------------------------- /unit/自定义1永不停止.js: -------------------------------------------------------------------------------- 1 | var {default_config, storage_name} = require('../config.js') 2 | var configStorage = storages.create(storage_name) 3 | var FileUtils = require("../lib/FileUtils.js") 4 | var commonFunctions = require("../lib/CommonFunction.js") 5 | Object.keys(default_config).forEach((key)=>{ 6 | log(key + ":" + configStorage.get(key)) 7 | }) 8 | 9 | configStorage.put("never_stop", true) 10 | configStorage.put("is_cycle", false) 11 | toastLog("配置完毕done") 12 | commonFunctions.killRunningScript() 13 | commonFunctions.setUpAutoStart(0.1) -------------------------------------------------------------------------------- /unit/自定义2计时停止.js: -------------------------------------------------------------------------------- 1 | var {default_config, storage_name} = require('../config.js') 2 | var configStorage = storages.create(storage_name) 3 | var FileUtils = require("../lib/FileUtils.js") 4 | var commonFunctions = require("../lib/CommonFunction.js") 5 | Object.keys(default_config).forEach((key)=>{ 6 | log(key + ":" + configStorage.get(key)) 7 | }) 8 | 9 | configStorage.put("never_stop", false) 10 | configStorage.put("is_cycle", false) 11 | toastLog("配置完毕done") 12 | commonFunctions.killRunningScript() 13 | commonFunctions.setUpAutoStart(0.1) -------------------------------------------------------------------------------- /update/glb.dex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonyJiangWJ/Ant-Forest-autoscript/ae1045091ae65215ad244a5dc884ab7c78ff9369/update/glb.dex -------------------------------------------------------------------------------- /update/检测更新.js: -------------------------------------------------------------------------------- 1 | let FileUtils = require('../lib/FileUtils.js') 2 | let loadingDialog = null 3 | try { 4 | importClass(com.tony.DownloaderListener) 5 | } catch (e) { 6 | toastLog('未载入dex, 请稍等') 7 | loadingDialog = dialogs.build({ 8 | title: '正在加载dex', 9 | content: '请稍等...' 10 | }).show() 11 | runtime.loadDex('./glb.dex') 12 | loadingDialog.setContent('加载完成!') 13 | sleep(1000) 14 | loadingDialog.dismiss() 15 | engines.execScriptFile(engines.myEngine().getSource()) 16 | exit() 17 | } 18 | 19 | importClass(com.tony.Downloader) 20 | importClass(com.tony.DownloaderListener) 21 | 22 | let apiUrl = 'https://api.github.com/repos/TonyJiangWJ/Ant-Forest/releases/latest' 23 | let targetOutputDir = FileUtils.getRealMainScriptPath(true) 24 | let downloader = new Downloader() 25 | log('下载并解压文件到目录:' + targetOutputDir) 26 | // 设置尝试获取总大小的次数,默认5次,github的content-length偶尔会给 偶尔不会给,主要原因是服务端用了分块传输的缘故 27 | // downloader.setTryCount(5) 28 | downloader.setTargetReleasesApiUrl(apiUrl) 29 | downloader.setOutputDir(targetOutputDir) 30 | // 设置不需要解压覆盖的文件 31 | // 请勿移除'update/glb.dex' 否则引起报错 32 | downloader.setUnzipSkipFiles(['.gitignore', 'update/glb.dex']) 33 | // 设置不需要备份的文件 34 | downloader.setBackupIgnoreFiles([]) 35 | 36 | loadingDialog = dialogs.build({ 37 | title: '正在请求网络', 38 | content: '加载中,请稍等...' 39 | }).show() 40 | let summary = downloader.getUpdateSummary() 41 | if (summary === null) { 42 | loadingDialog.setContent('无法获取release版本信息') 43 | sleep(1000) 44 | loadingDialog.dismiss() 45 | exit() 46 | } 47 | summary = JSON.parse(summary) 48 | let localVersion = downloader.getLocalVersion() 49 | let content = '线上版本:' + summary.tagName + '\n' 50 | content += '本地版本:' + (localVersion === null ? '无法获取本地版本信息' : localVersion) + '\n' 51 | content += '更新内容:\n' + summary.body 52 | 53 | loadingDialog.dismiss() 54 | 55 | 56 | let downloadDialog = dialogs.build({ 57 | title: '更新中...', 58 | content: '更新中', 59 | progress: { 60 | max: 100, 61 | horizontal: true, 62 | showMinMax: false 63 | } 64 | }) 65 | let setMaxProgress = false 66 | downloader.setListener(new DownloaderListener({ 67 | updateGui: function (string) { 68 | log(string) 69 | downloadDialog.setContent(string) 70 | }, 71 | updateProgress: function (progressInfo) { 72 | downloadDialog.setProgress(progressInfo.getProgress() * 100) 73 | } 74 | })) 75 | let downloadingExecutor = function (backup) { 76 | if (backup) { 77 | downloader.backup() 78 | sleep(1000) 79 | } 80 | downloader.downloadZip() 81 | downloadDialog.setContent('更新完成') 82 | sleep(2000) 83 | downloadDialog.dismiss() 84 | } 85 | dialogs.build({ 86 | title: '是否下载更新', 87 | content: content, 88 | 89 | neutral: '备份后更新', 90 | negative: '取消', 91 | positive: '覆盖更新', 92 | 93 | negativeColor: 'red', 94 | positiveColor: '#f9a01c', 95 | }) 96 | .on('negative', () => { 97 | exit() 98 | }) 99 | .on('neutral', () => { 100 | downloadDialog.show() 101 | threads.start(function () { downloadingExecutor(true) }) 102 | }) 103 | .on('positive', () => { 104 | downloadDialog.show() 105 | threads.start(function () { downloadingExecutor(false) }) 106 | }) 107 | .show() 108 | -------------------------------------------------------------------------------- /update/说明-重要.txt: -------------------------------------------------------------------------------- 1 | “自动下载更新脚本”更新之后需要手动关闭AutoJS 2 | 否则新的dex没法覆盖加载。 3 | 具体操作以MIUI为例,上划打开后台 长按AutoJS, 4 | 设置 选择结束运行,而不仅仅是滑动关闭。 5 | 理论上Pro版已经支持覆盖加载,免费版不支持。 6 | 但是为了保险起见都进行一遍强制停止。 7 | 8 | 可以手动修改第23行 9 | 将解压路径修改为你喜欢的文件夹 10 | let targetOutputDir = files.cwd() 11 | 比如修改为如下 12 | let targetOutputDir = '/storage/0/脚本/你喜欢的路径' 13 | 下载完成后新代码就会到 14 | '/storage/0/脚本/你喜欢的路径'里面 15 | 16 | 执行完毕后需要在 targetOutputDir中下拉刷新,否则看不到 17 | 18 | 19 | 本 “自动下载更新脚本” 同样适用于 20 | 所有github/releases发布的最新脚本 21 | 仅仅需要修改21行 22 | let apiUrl = "目标脚本更新API" 23 | "目标脚本更新API" 需要自己去获取 24 | 格式如下: 25 | 'https://api.github.com/repos/${github用户名}/${github仓库名}/releases/latest' 26 | 当然如果没有发布releases 下载也是无效的 -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | {"version":"v1.1.1.4","nodeId":"undefined"} --------------------------------------------------------------------------------