diff --git a/.github/ISSUE_TEMPLATE/bug-report-------.md b/.github/ISSUE_TEMPLATE/bug-report-------.md new file mode 100644 index 0000000..51b6d8d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-------.md @@ -0,0 +1,36 @@ +--- +name: Bug report / 缺陷上报 +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug / 问题描述** +A clear and concise description of what the bug is. / 相关问题的描述 + +**To Reproduce / 复现流程** +Steps to reproduce the behavior: / 问题重现流程 +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +If possible, please use video to reproduce the behavior / 可以的话,录一个问题重现的视频 + +**Error Stack/错误堆栈** +Error stacks related to this bug. You can find this in `/sdcard/solopi/error` and `/sdcard/Android/data/com.alipay.hulu/cache/logs/` / 与问题相关的错误堆栈。你可以在 `/sdcard/solopi/error` 和 `/sdcard/Android/data/com.alipay.hulu/cache/logs/` 找到。 + +**Screenshots / 截图** +If applicable, add screenshots to help explain your problem. / 最好能够附上相关问题的截图信息。 + +**Device Info / 设备信息** + - Manufacturer/生产厂家: [e.g. Huawei] + - Device/设备: [e.g. Huawei P30] + - OS/系统版本: [e.g. Android 9.0] + - CPU Structure/CPU架构: [e.g. arm64 v8a] + - SoloPi Version/SoloPi版本 [e.g. 0.9.2] + +**Additional context/其他内容** +Add any other context about the problem here. / 其他与问题相关的内容 diff --git a/.github/ISSUE_TEMPLATE/feature-request-------.md b/.github/ISSUE_TEMPLATE/feature-request-------.md new file mode 100644 index 0000000..a195535 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request-------.md @@ -0,0 +1,17 @@ +--- +name: Feature request / 功能建议 +about: Suggest an idea for this project +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe. / 是否为了解决现有问题?请描述相关问题** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like / 描述下你的解决方案** +A clear and concise description of what you want to happen. + +**Additional context/额外信息** +Add any other context or screenshots about the feature request here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22d7ff8..f4f04bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -## Contributing to Soloπ +## Contributing to SoloPi Soloπ is released under the Apache 2.0 license, and follows a very standard Github development process, using Github tracker for issues and merging pull requests into master . If you would like to contribute something, or simply want to hack on the code this document should help you get started. diff --git a/Disclaimer.md b/Disclaimer.md index 515d885..81ff6ba 100644 --- a/Disclaimer.md +++ b/Disclaimer.md @@ -1,6 +1,6 @@ -Soloπ开源自动化测试工具的功能是我们针对不同用户的需求而特别提供。Soloπ提供的应用、代码和资料的著作权均归Soloπ所有,用户具有自由的使用权。 +SoloPi开源自动化测试工具的功能是我们针对不同用户的需求而特别提供。SoloPi提供的应用、代码和资料的著作权均归SoloPi所有,用户具有自由的使用权。 -如果用户下载、安装、使用、修改本工具及相关代码,即表明用户信任该工具。那么,用户在使用本工具时造成对用户自己或他人任何形式的损失和伤害,Soloπ工具不承担任何责任。 +如果用户下载、安装、使用、修改本工具及相关代码,即表明用户信任该工具。那么,用户在使用本工具时造成对用户自己或他人任何形式的损失和伤害,SoloPi工具不承担任何责任。 本工具不含有任何旨在破坏用户计算机数据和获取用户隐私信息的恶意代码;本工具使用过程中获取到的操作信息在打印日志时默认会进行脱敏,且不会进行上传;当应用出现异常时,用户可手动选择是否通过邮件上报故障日志,不会泄漏用户隐私。 diff --git a/NOTICE.md b/NOTICE.md index 047be4e..69cce70 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -444,6 +444,27 @@ limitations under the License. > Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0 +## scrcpy (Modified) + +> https://github.com/Genymobile/scrcpy + +Copyright (C) 2018 Genymobile +Copyright (C) 2018-2019 Romain Vimont + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +> Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0 + ## 部分SVG图片来源 > https://www.iconfont.cn diff --git a/README.md b/README.md index 290dfe2..26be6cc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ -# Soloπ +# SoloPi [![GitHub stars](https://img.shields.io/github/stars/soloPi/SoloPi.svg)](https://github.com/soloPi/SoloPi/stargazers) [![GitHub license](https://img.shields.io/github/license/soloPi/SoloPi.svg)](https://github.com/soloPi/SoloPi/blob/master/LICENSE) [![GitHub release](https://img.shields.io/github/release/alipay/SoloPi.svg)](https://github.com/soloPi/SoloPi/releases) [![API](https://img.shields.io/badge/API-18%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=18) [![TesterHome](https://img.shields.io/badge/TTF-TesterHome-2955C5.svg)](https://testerhome.com/opensource_projects/82) -> Soloπ是一个无线化、非侵入式的Android自动化工具,公测版拥有录制回放、性能测试、一机多控三项主要功能,能为测试开发人员节省宝贵时间。 +### English Version: [README](./README_eng.md) + +> SoloPi是一个无线化、非侵入式的Android自动化工具,公测版拥有录制回放、性能测试、一机多控三项主要功能,能为测试开发人员节省宝贵时间。 +> SoloPi新增鸿蒙版本,欢迎大家试用,切到 solopi-harmony分支 ### 功能特性 @@ -15,7 +18,9 @@ **[Native应用录制回放使用视频](https://gw.alipayobjects.com/os/basement_prod/3472d35c-bd57-4c82-8112-5dcde42fcb32.mov)** -Soloπ拥有录制操作的能力,用户只需要通过Soloπ执行用例步骤,Soloπ就能够将用户的操作记录下来,并且支持在各个设备上进行回放,这一切都能够在手机上独立完成。详见[录制回放](../../wikis/RecordCase)一篇。 +SoloPi拥有录制操作的能力,用户只需要通过SoloPi执行用例步骤,SoloPi就能够将用户的操作记录下来,并且支持在各个设备上进行回放,这一切都能够在手机上独立完成。详见[录制回放](../../wikis/RecordCase)一篇。 + +SoloPi JSON 可以转化为其他自动化脚本,目前支持 Appium 和 Macaca ,可以前往 https://github.com/soloPi/SoloPi-Convertor 下载体验,欢迎Watch、Star、Fork 三连。 #### 性能工具 @@ -25,9 +30,9 @@ Soloπ拥有录制操作的能力,用户只需要通过Soloπ执行用例步 **[响应耗时计算使用视频](https://gw.alipayobjects.com/os/basement_prod/4e82ca85-13fc-4de2-82ff-a9079344f5ef.mov)** -Soloπ能够记录待测应用的各项指标,你可以在悬浮窗中观察实时更新的数据,也可以对性能数据进行录制,在录制结束后查看图表;同时,Soloπ还支持性能加压,能够对CPU、内存与网络环境进行限制,复现应用在性能较差、网络环境不佳场景下的表现。 +SoloPi能够记录待测应用的各项指标,你可以在悬浮窗中观察实时更新的数据,也可以对性能数据进行录制,在录制结束后查看图表;同时,SoloPi还支持性能加压,能够对CPU、内存与网络环境进行限制,复现应用在性能较差、网络环境不佳场景下的表现。 -除了常规性能指标,Soloπ还提供了启动耗时计算工具,测试同学只需要点击两次按钮,就可以得到最贴近用户体验的启动耗时数据。同时,启动耗时计算工具还可以通过广播调用,可以非常方便的与UI自动化测试打通。详见[性能工具](../../wikis/Performance)一篇。 +除了常规性能指标,SoloPi还提供了启动耗时计算工具,测试同学只需要点击两次按钮,就可以得到最贴近用户体验的启动耗时数据。同时,启动耗时计算工具还可以通过广播调用,可以非常方便的与UI自动化测试打通。详见[性能工具](../../wikis/Performance)一篇。 #### 一机多控 @@ -35,7 +40,7 @@ Soloπ能够记录待测应用的各项指标,你可以在悬浮窗中观察 **[一机多控使用视频](https://gw.alipayobjects.com/os/basement_prod/971b5467-3db0-4781-86e3-15b3907323f6.mov)** -Soloπ支持通过操作一台主机设备来控制多台从机设备,不需要在各个设备上分别进行重复冗杂的兼容性测试,能够极大提升兼容性测试的效率。详见[一机多控](../../wikis/OneToMany)一篇。 +SoloPi支持通过操作一台主机设备来控制多台从机设备,不需要在各个设备上分别进行重复冗杂的兼容性测试,能够极大提升兼容性测试的效率。详见[一机多控](../../wikis/OneToMany)一篇。 @@ -46,10 +51,11 @@ Soloπ支持通过操作一台主机设备来控制多台从机设备,不需 #### 编译环境: - macOS 10.14.3 -- Android Studio 3.2 -- **Gradle 4.4(Android Studio打开项目时会提示升级Gradle版本,建议不要进行升级)** -- Ndk 15.2.4203819 -- TargetApi 25 +- Android Studio 4.0 +- Gradle 6.1.1 +- CMake 3.6/3.10均可 +- Ndk 16 +- TargetApi 29 - MinimumApi 18 - **注意,构建时请将Android Studio的instant run功能关闭,否则打出来的安装包会无法使用** @@ -75,13 +81,15 @@ Soloπ支持通过操作一台主机设备来控制多台从机设备,不需 对于VIVO设备,如果在开发者选项中包含“USB安全操作”,需要手动进行开启,否则录制回放与一机多控功能可能会无法正常操作 - 对于小米设备,需要开启开发者选项中的`USB安装`与`USB调试(安全设置)`,否则录制回放与一机多控功能会无法正常操作;此外,还需要手动开启Soloπ应用权限中的`后台弹出界面`选项,否则无法正常使用 + 对于小米设备,需要开启开发者选项中的`USB安装`与`USB调试(安全设置)`,否则录制回放与一机多控功能会无法正常操作;此外,还需要手动开启SoloPi应用权限中的`后台弹出界面`选项,否则无法正常使用 对于魅族设备,如果待测应用属于支付、金融类应用,需要在手机管家中关闭安全支付功能,否则录制回放与一机多控功能可能会无法正常操作 对于华为设备,需要开启开发者选项中的 `"仅充电"模式下允许ADB调试`,否则断开USB线后会提示adb调试中断 - 对于OPPO设备,系统会10分钟自动断开USB调试,导致Soloπ不可用。如果想要保持调试稳定,需要将设备连接到电脑 + 对于OPPO设备,系统会10分钟自动断开USB调试,导致SoloPi不可用。如果想要保持调试稳定,需要将设备连接到电脑 + + **如果设备有安全输入法,请前往`系统设置->输入法`关闭安全输入法,否则例如密码等一些输入框无法正常输入** #### 连接设备并开启wifi调试端口 @@ -143,13 +151,13 @@ Soloπ支持通过操作一台主机设备来控制多台从机设备,不需 $ANDROID_SDK/platform-tools/adb -s ${之前记录的序列号} tcpip 5555 ``` -**下载打包好的Soloπ APK(Soloπ.apk文件),或者clone源码在本地编译,具体在Soloπ中的操作可以参考: [第一次使用](../../wikis/FirstUse)** +**下载打包好的SoloPi APK(SoloPi.apk文件),或者clone源码在本地编译,具体在SoloPi中的操作可以参考: [第一次使用](../../wikis/FirstUse)** ### 文档 -- 如果你是第一次使用Soloπ,推荐你先了解Soloπ的一些[使用注意事项](../../wikis/FirstUse) +- 如果你是第一次使用SoloPi,推荐你先了解SoloPi的一些[使用注意事项](../../wikis/FirstUse) - Wiki文档: [Home](../../wikis/home) @@ -172,18 +180,20 @@ Soloπ支持通过操作一台主机设备来控制多台从机设备,不需 - 钉钉群(一群已满,请添加二群): -![group](https://gw.alipayobjects.com/mdn/rms_e29b5f/afts/img/A*K6wzRZfxDv8AAAAAAAAAAABkARQnAQ) +![group](https://gw.alipayobjects.com/mdn/rms_e29b5f/afts/img/A*JrDwQ4qkVBcAAAAAAAAAAAAAARQnAQ) * 微信群: **目前微信群已满,推荐加入钉钉群** **除了钉钉群外,我们在TesterHome也有相关板块,可以在社区里留言回复 https://testerhome.com/topics/node152 ** +### 贡献 +SoloPi 需要开发者们的共建,也希望能在开发者的支持下更好的发展,如果你基于SoloPi开发出了更贴近业务场景的能力(商业/非商业),欢迎和我们联系,也希望能主动为开源出力,提交各种 features/bugfix/issue ,共同维护SoloPi这套自动化工具。 ### 如何贡献 - [代码贡献](CONTRIBUTING.md) : Soloπ 开发参与说明书 + [代码贡献](CONTRIBUTING.md) : SoloPi 开发参与说明书 独乐乐不如众乐乐,开源的核心还是在于技术的分享交流,当你对开源项目产生了一些想法时,有时还会有更加Smart的表达方式,比如(Thanks to uiautomator2): diff --git a/README_eng.md b/README_eng.md new file mode 100644 index 0000000..1e07089 --- /dev/null +++ b/README_eng.md @@ -0,0 +1,208 @@ +# SoloPi + +[![GitHub stars](https://img.shields.io/github/stars/soloPi/SoloPi.svg)](https://github.com/soloPi/SoloPi/stargazers) [![GitHub license](https://img.shields.io/github/license/soloPi/SoloPi.svg)](https://github.com/soloPi/SoloPi/blob/master/LICENSE) [![GitHub release](https://img.shields.io/github/release/alipay/SoloPi.svg)](https://github.com/soloPi/SoloPi/releases) [![API](https://img.shields.io/badge/API-18%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=18) [![TesterHome](https://img.shields.io/badge/TTF-TesterHome-2955C5.svg)](https://testerhome.com/opensource_projects/82) + +> SoloPi is a wireless, non-invasive testing tool for automatic Android software testing. The Beta version has 3 main features: record and replay, performance testing, multi-device compatibility testing(OneToMany). + +### [Features](#1)
+### [Getting started](#2)
+### [Folders and description](#3)
+### [Contributing](#4)
+### [Attributions](#5)
+### [License](#6)
+### [Disclaimer](#7)
+ + +## Features + +### 1. Record and replay + +SoloPi captures all actions performed during tesing sessions so that issues can be identified and resolved more quickly. The recording can be played on any devices. All these actions can be done on just one single phone. + +![Recording playback](assets/replay.gif) + +The video tutorial: + +**[Record the testing on a mobile game.](https://gw.alipayobjects.com/mdn/rms_e29b5f/afts/file/A*ym07T6nACDIAAAAAAAAAAABkARQnAQ)** + +**[Record the testing on a native phone app.](https://gw.alipayobjects.com/os/basement_prod/3472d35c-bd57-4c82-8112-5dcde42fcb32.mov)** + + +### 2. Performance testing + +* SoloPi is able to record and show the app's performance data such as CPU, memory, internet speed while do the testing. The performance window with selected testing metrics will float on top. After testing, you can check each testing parameter with generated data graphs. + +* Besides, SoloPi can change testing environment to simulate certain situations. For instance, slow down the internet speed to simulate a situation when the internet is bad while using the app. + +* SoloPi also add a function to calculate app launch time. This tool to the most extent, shows the actual launch time. This calculator function can be incorporated with UI automatic tests by sending broadcast messages. + +![Performance analysis](assets/performance.gif) + +The video tutorial: + +**[Use the performance analysis function](https://gw.alipayobjects.com/os/basement_prod/1996390b-9ec8-4046-8ce8-459afa05d6c5.mov)** + +**[Use the launch time calculator](https://gw.alipayobjects.com/os/basement_prod/4e82ca85-13fc-4de2-82ff-a9079344f5ef.mov)** + +### 3. Multi-device compatibility testing + +SoloPi supports simultaneous multi-device compatibility testing which is controlled by one device. So it enormously improves the efficiency of testing on different devices. + +![Multi-device testing](assets/oneToMany.gif) + +The video tutorial: + +**[Simultaneous multi-device testing](https://gw.alipayobjects.com/os/basement_prod/971b5467-3db0-4781-86e3-15b3907323f6.mov)** + +## Getting started + +> Open source SoloPi excludes the multi-device compatibility testing feature since it's still unstable. + +### 1. Establishing a build environment + +- macOS 10.14.3 +- Android Studio 3.2 +- **Gradle 4.4(Upgrading is not recommended.)** +- **CMake 3.6.4111459(Upgrading is not recommended.)** +- Ndk 15.2.4203819 +- TargetApi 25 +- MinimumApi 18 +- **Note: Turn off instant run function in Android Studio. Otherwise the app does not work.** + +### 2. Downloading and setting Android SDK path + +- Download SDK Platform [here](https://developer.android.com/studio/releases/platform-tools#downloads). + +- Unzip it and add the path to the system environment variable `ANDROID_SDK=${sdk path}` . You can also refer to articles such as how to set adb system environment variable. + +**NOTE:** +For system above Windows 10, it takes effect immediately in a new command line window, while for older versions of system, you need to restart the computer. For Linux and MacOS, you can test if it works with `echo $ANDROID_SDK`. + +### 3. Turning on on-device developer mode + +- Open the Settings app. +- (Only on Android 8.0 or higher) Select System. +- Scroll to the bottom and select About phone. +- Scroll to the bottom and tap Build number 7 times. The system will show ‘You are now a developer.’ (messages may vary.) +- Return to the previous screen to find Developer options near the bottom. Toggle the options on and enable USB debugging + +### 4. Known issues + +- For VIVO devices, if there’s an option like ‘USB security access’ under developer options, it needs to be toggled on, otherwise recording and multi-device testing function may not work. + +- For Xiaomi devices, under developer options, USB installation and USB debugging also need to be toggled on. Besides, you also need to turn on ‘后台弹出界面’ permission of SoloPi (System Settings -> App Management -> SoloPi -> Permissions). + +- For MEIZU devices, if the application to be tested contains highly secured functions like payment function, the secure payment function in the system needs to be turned off. + +- For HUAWEI devices, under developer options, you need to turn on ‘USB debugging’ and ‘allow ADB debugging in charge only mode’ option. Otherwise, when the USB cable is unplugged, the ADB debugging is also shut down. + +- For OPPO devices, system would ‘unchecking’ the ‘USB debugging’ every 10 minutes, leading to the unavailability of SoloPi. To solve it, keep connecting the phone to the computer. + +- **It's highy recommandded to turn off safety input method in system language settings (if it has), otherwise text input may not work when input password or something else.** + +### 5. Debugging apps over Wi-Fi + +#### 5.1 Connect the device to PC via USB and make sure debugging is working. + +When the device is connected to the PC, the device should pop up 'Allow USB debugging?' or similar messages. Click 'Yes'. + +Check if the connection is successful in command line: + +Windows: +```bash + %ANDROID_SDK%\platform-tools\adb.exe devices +``` +MacOS/Linux: +```shell + $ANDROID_SDK/platform-tools/adb devices +``` + +If it returns with the device number, then the connection is successful. + +#### 5.2 Make the connection + +> **Note:** Windows system may need Android device driver to make a successful connection. Devices driver can be downloaded on device's official website. You can also download the phone manager which includes device driver. + +> **Note:** If the command line dosen't return `device`, make sure the device driver is installed successfully and the USB debugging is turned on. For some device, the connection mode needs to be `Media Transfer Protocal`(MTP). + +For single device, + +Windows: +```bash + %ANDROID_SDK%\platform-tools\adb.exe tcpip 5555 +``` + +macOS/Linux: +```shell + $ANDROID_SDK/platform-tools/adb tcpip 5555 +``` + +The device may show `restarting in TCP mode port: 5555` to remind you the wi-fi ADB debugging mode is on. + +For multiple devices, + +Find the device number which is the serial number before `device` and save it. + +Windows: + +```bash + %ANDROID_SDK%\platform-tools\adb.exe -s ${serial number} tcpip 5555 +``` +macOS/Linux: + +```shell + $ANDROID_SDK/platform-tools/adb -s ${serial number} tcpip 5555 +``` + +#### 5.3 Downloading SoloPi + +You can either download SoloPi.apk or clone the repository. + +## Folders and description +In the folder src, +- app: The business logic of the application. +- shared: The core function of the application. +- common: The application architecture. +- mdlibrary: Proxy generation of ExportService. +- permission: Permission management. +- AdbLib: ADB connection. +- androidWebscoket: Android WebSocket. + +## Contributing + +This project is mainly open to developers who want to do software testing. If you have any suggestions or questions, you can open an issue, send a PR, or leave a message at our page at [TesterHome](https://testerhome.com/topics/node152). + +If you like our project, please fork/⭐Star this project! + +## Attributions + +We want to thank those [third party libraries](https://github.com/ruoranw/SoloPi/blob/master/NOTICE.md) used in this project without which this project couldn't be completed. + +## License + +This project is under the Apache 2.0 License. See the [LICENSE](LICENSE) file for the full license text. + +```text +Copyright (C) 2015-present, Ant Financial Services Group + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + + +## Disclaimer + +[Disclaimer](Disclaimer.md) + + + + diff --git a/arm64-v8a.json b/arm64-v8a.json index 2b15c99..cca40b7 100644 --- a/arm64-v8a.json +++ b/arm64-v8a.json @@ -16,10 +16,16 @@ "name": "hulu_screenRecord" }, { - "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap.v8a.zip", - "version": 3, + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_7.zip", + "version": 7, "type": "base", "name": "minicap" + }, + { + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/scrcpytouch.zip", + "version": 2, + "type": "base", + "name": "scrcpytouch" } ] -} \ No newline at end of file +} diff --git a/armeabi-v7a.json b/armeabi-v7a.json index 8dc35c8..baa8f22 100644 --- a/armeabi-v7a.json +++ b/armeabi-v7a.json @@ -16,10 +16,16 @@ "name": "hulu_screenRecord" }, { - "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap.v7a.zip", - "version": 3, + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_7.zip", + "version": 7, "type": "base", "name": "minicap" + }, + { + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/scrcpytouch.zip", + "version": 2, + "type": "base", + "name": "scrcpytouch" } ] -} \ No newline at end of file +} diff --git a/armeabi.json b/armeabi.json index d978329..f6be6ef 100644 --- a/armeabi.json +++ b/armeabi.json @@ -16,10 +16,16 @@ "name": "hulu_screenRecord" }, { - "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap.v7a.zip", - "version": 3, + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/hulu_minicap_7.zip", + "version": 7, "type": "base", "name": "minicap" + }, + { + "url": "https://raw.githubusercontent.com/alipay/SoloPi/master/plugins/scrcpytouch.zip", + "version": 2, + "type": "base", + "name": "scrcpytouch" } ] -} \ No newline at end of file +} diff --git a/plugins/hulu_minicap.v7a.zip b/plugins/hulu_minicap.v7a.zip deleted file mode 100644 index 5d89b68..0000000 Binary files a/plugins/hulu_minicap.v7a.zip and /dev/null differ diff --git a/plugins/hulu_minicap.v8a.zip b/plugins/hulu_minicap.v8a.zip deleted file mode 100644 index 4ed9d96..0000000 Binary files a/plugins/hulu_minicap.v8a.zip and /dev/null differ diff --git a/plugins/hulu_minicap_7.zip b/plugins/hulu_minicap_7.zip new file mode 100644 index 0000000..bebf006 Binary files /dev/null and b/plugins/hulu_minicap_7.zip differ diff --git a/plugins/scrcpytouch.zip b/plugins/scrcpytouch.zip new file mode 100644 index 0000000..2ca0775 Binary files /dev/null and b/plugins/scrcpytouch.zip differ diff --git a/src/.gitignore b/src/.gitignore index f922cb1..62ef981 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -3,3 +3,9 @@ build/ .gradle/ *.iml local.properties + +.cxx/ + +git_stats/ +seeds.txt +unused.txt diff --git a/src/AdbLib/src/com/cgutman/adblib/AdbConnection.java b/src/AdbLib/src/com/cgutman/adblib/AdbConnection.java index c834a17..19df3cd 100644 --- a/src/AdbLib/src/com/cgutman/adblib/AdbConnection.java +++ b/src/AdbLib/src/com/cgutman/adblib/AdbConnection.java @@ -1,15 +1,12 @@ package com.cgutman.adblib; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.Socket; -import java.util.HashMap; /** * This class represents an ADB connection. @@ -20,69 +17,68 @@ public class AdbConnection implements Closeable { /** The underlying socket that this class uses to * communicate with the target device. */ - private Socket socket; - + protected Socket socket; + /** The last allocated local stream ID. The ID * chosen for the next stream will be this value + 1. */ - private int lastLocalId; - + protected int lastLocalId; + /** * The input stream that this class uses to read from * the socket. */ - private InputStream inputStream; - + protected InputStream inputStream; + /** * The output stream that this class uses to read from * the socket. */ - OutputStream outputStream; - + protected OutputStream outputStream; + /** * The backend thread that handles responding to ADB packets. */ - private Thread connectionThread; - + protected Thread connectionThread; + /** * Specifies whether a connect has been attempted */ - private boolean connectAttempted; - + protected boolean connectAttempted; + /** * Specifies whether a CNXN packet has been received from the peer. */ - private boolean connected; - + protected boolean connected; + /** * Specifies the maximum amount data that can be sent to the remote peer. * This is only valid after connect() returns successfully. */ - private int maxData; - + protected int maxData; + /** * An initialized ADB crypto object that contains a key pair. */ - private AdbCrypto crypto; + protected AdbCrypto crypto; /** * Specifies whether this connection has already sent a signed token. */ - private boolean sentSignature; - - /** - * A hash map of our open streams indexed by local ID. - **/ - private HashMap openStreams; + protected boolean sentSignature; + + protected volatile boolean isFine = true; + + protected AdbMessageManager msgManager; + + protected volatile boolean stopFlag = false; - private volatile boolean isFine = true; - /** * Internal constructor to initialize some internal state */ private AdbConnection() { - openStreams = new HashMap(); + msgManager = new AdbMessageManager(this); lastLocalId = 0; connectionThread = createConnectionThread(); } @@ -104,11 +100,20 @@ public static AdbConnection create(Socket socket, AdbCrypto crypto) throws IOExc newConn.socket = socket; // 试试bufferedStream - newConn.inputStream = new BufferedInputStream(socket.getInputStream()); - newConn.outputStream = new BufferedOutputStream(socket.getOutputStream()); - + newConn.inputStream = socket.getInputStream(); + newConn.outputStream = socket.getOutputStream(); + /* Disable Nagle because we're sending tiny packets */ socket.setTcpNoDelay(true); + + // 写入缓冲区16K + socket.setSendBufferSize(16 * 1024); + + // 读取缓冲区64K + socket.setReceiveBufferSize(64 * 1024); + socket.setTrafficClass(0x10); + + socket.setPerformancePreferences(0, 2, 1); return newConn; } @@ -123,114 +128,14 @@ private Thread createConnectionThread() return new Thread(new Runnable() { @Override public void run() { - while (!connectionThread.isInterrupted()) + while (!stopFlag && !connectionThread.isInterrupted()) { try { /* Read and parse a message off the socket's input stream */ AdbProtocol.AdbMessage msg = AdbProtocol.AdbMessage.parseAdbMessage(inputStream); /* Verify magic and checksum */ - if (!AdbProtocol.validateMessage(msg)) - continue; - - String cmd = null; - - switch (msg.command) - { - /* Stream-oriented commands */ - case AdbProtocol.CMD_OKAY: - case AdbProtocol.CMD_WRTE: - case AdbProtocol.CMD_CLSE: - /* We must ignore all packets when not connected */ - if (!conn.connected) - continue; - - /* Get the stream object corresponding to the packet */ - AdbStream waitingStream = openStreams.get(msg.arg1); - if (waitingStream == null) - continue; - - synchronized (waitingStream) { - if (msg.command == AdbProtocol.CMD_OKAY) - { - /* We're ready for writes */ - waitingStream.updateRemoteId(msg.arg0); - waitingStream.readyForWrite(); - - /* Unwait an open/write */ - waitingStream.notify(); - - cmd = "OKAY"; - } - else if (msg.command == AdbProtocol.CMD_WRTE) - { - /* Got some data from our partner */ - waitingStream.addPayload(msg.payload); - - /* Tell it we're ready for more */ - waitingStream.sendReady(); - cmd = "WRTE"; - } - else if (msg.command == AdbProtocol.CMD_CLSE) - { - /* He doesn't like us anymore :-( */ - conn.openStreams.remove(msg.arg1); - - /* Notify readers and writers */ - waitingStream.notifyClose(); - cmd = "CLSE"; - } - } - - break; - - case AdbProtocol.CMD_AUTH: - - byte[] packet; - - cmd = "AUTH"; - - if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN) - { - /* This is an authentication challenge */ - if (conn.sentSignature) - { - /* We've already tried our signature, so send our public key */ - packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC, - conn.crypto.getAdbPublicKeyPayload()); - } - else - { - /* We'll sign the token */ - packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE, - conn.crypto.signAdbTokenPayload(msg.payload)); - conn.sentSignature = true; - } - - /* Write the AUTH reply */ - conn.outputStream.write(packet); - conn.outputStream.flush(); - } - break; - - case AdbProtocol.CMD_CNXN: - synchronized (conn) { - cmd = "CNXN"; - /* We need to store the max data size */ - conn.maxData = msg.arg1; - - /* Mark us as connected and unwait anyone waiting on the connection */ - conn.connected = true; - conn.notifyAll(); - } - break; - - default: - cmd = "default"; - /* Unrecognized packet, just drop it */ - break; - } - + msgManager.pushMessage(msg); //System.out.println("Receive CMD:" + cmd + "; arg0 " + msg.arg0 + "; arg1: " + msg.arg1 + "; data: " + msg.payloadLength); } catch (Exception e) { @@ -240,6 +145,8 @@ else if (msg.command == AdbProtocol.CMD_CLSE) } } + stopFlag = false; + /* This thread takes care of cleaning up pending streams */ synchronized (conn) { cleanupStreams(); @@ -332,19 +239,21 @@ public AdbStream open(String destination) throws UnsupportedEncodingException, I throw new IllegalStateException("connect() must be called first"); /* Wait for the connect response */ - synchronized (this) { - if (!connected) - wait(); - - if (!connected) { - throw new IOException("Connection failed"); + if (!connected) { + synchronized (this) { + if (!connected) + wait(); + + if (!connected) { + throw new IOException("Connection failed"); + } } } /* Add this stream to this list of half-open streams */ AdbStream stream = new AdbStream(this, localId); - openStreams.put(localId, stream); - + msgManager.addAdbStream(localId, stream); + /* Send the open */ outputStream.write(AdbProtocol.generateOpen(localId, destination)); outputStream.flush(); @@ -367,17 +276,7 @@ public AdbStream open(String destination) throws UnsupportedEncodingException, I * This function terminates all I/O on streams associated with this ADB connection */ private void cleanupStreams() { - /* Close all streams on this connection */ - for (AdbStream s : openStreams.values()) { - /* We handle exceptions for each close() call to avoid - * terminating cleanup for one failed close(). */ - try { - s.close(); - } catch (IOException e) {} - } - - /* No open streams anymore */ - openStreams.clear(); + msgManager.cleanupStreams(); } /** This routine closes the Adb connection and underlying socket @@ -399,7 +298,7 @@ public void close() throws IOException { } catch (InterruptedException e) { } } - public synchronized boolean isFine() { + public boolean isFine() { return isFine && connectAttempted && connected; } diff --git a/src/AdbLib/src/com/cgutman/adblib/AdbMessageManager.java b/src/AdbLib/src/com/cgutman/adblib/AdbMessageManager.java new file mode 100644 index 0000000..906632f --- /dev/null +++ b/src/AdbLib/src/com/cgutman/adblib/AdbMessageManager.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.cgutman.adblib; + +import java.io.IOException; +import java.util.HashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class AdbMessageManager { + + /** + * A hash map of our open streams indexed by local ID. + **/ + private HashMap openStreams; + + /** + * 调度任务 + */ + private ExecutorService executorService; + + private AdbConnection conn; + + private LinkedBlockingQueue msgQueue; + + protected AdbMessageManager(AdbConnection conn) { + this.openStreams = new HashMap<>(); + this.conn = conn; + this.msgQueue = new LinkedBlockingQueue<>(); + + // 三个线程处理消息 + executorService = new ThreadPoolExecutor(5, Integer.MAX_VALUE, + 0, TimeUnit.MILLISECONDS, + new SynchronousQueue()); + executorService.execute(getMessageHandler()); + executorService.execute(getMessageHandler()); + executorService.execute(getMessageHandler()); + } + + /** + * 添加消息 + * @param msg + */ + protected void pushMessage(AdbProtocol.AdbMessage msg) { + msgQueue.add(msg); + } + + private Runnable getMessageHandler() { + return new Runnable() { + @Override + public void run() { + while (true) { + try { + AdbProtocol.AdbMessage msg = msgQueue.poll(5000, TimeUnit.MILLISECONDS); + + if (msg != null) { + processAdbMessage(msg); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }; + } + + /** + * 添加adb stream + * @param localId + * @param stream + */ + protected void addAdbStream(int localId, AdbStream stream) { + openStreams.put(localId, stream); + } + + protected void cleanupStreams() { + /* Close all streams on this connection */ + for (AdbStream s : openStreams.values()) { + /* We handle exceptions for each close() call to avoid + * terminating cleanup for one failed close(). */ + try { + s.close(); + } catch (IOException e) {} + } + + /* No open streams anymore */ + openStreams.clear(); + } + + /** + * 处理ADB消息 + * @param msg + */ + private void processAdbMessage(AdbProtocol.AdbMessage msg) { + String cmd = null; + + if (!AdbProtocol.validateMessage(msg)) + return; + + try { + switch (msg.command) { + /* Stream-oriented commands */ + case AdbProtocol.CMD_OKAY: + case AdbProtocol.CMD_WRTE: + case AdbProtocol.CMD_CLSE: + /* We must ignore all packets when not connected */ + if (!conn.connected) + return; + + /* Get the stream object corresponding to the packet */ + AdbStream waitingStream = openStreams.get(msg.arg1); + if (waitingStream == null) + return; + + synchronized (waitingStream) { + if (msg.command == AdbProtocol.CMD_OKAY) { + /* We're ready for writes */ + waitingStream.updateRemoteId(msg.arg0); + waitingStream.readyForWrite(); + + /* Unwait an open/write */ + waitingStream.notify(); + + cmd = "OKAY"; + } else if (msg.command == AdbProtocol.CMD_WRTE) { + /* Got some data from our partner */ + waitingStream.addPayload(msg.payload); + + /* Tell it we're ready for more */ + waitingStream.sendReady(); + cmd = "WRTE"; + } else if (msg.command == AdbProtocol.CMD_CLSE) { + /* He doesn't like us anymore :-( */ + openStreams.remove(msg.arg1); + + /* Notify readers and writers */ + waitingStream.notifyClose(); + cmd = "CLSE"; + } + } + + break; + + case AdbProtocol.CMD_AUTH: + + byte[] packet; + + cmd = "AUTH"; + + if (msg.arg0 == AdbProtocol.AUTH_TYPE_TOKEN) { + /* This is an authentication challenge */ + if (conn.sentSignature) { + /* We've already tried our signature, so send our public key */ + packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_RSA_PUBLIC, + conn.crypto.getAdbPublicKeyPayload()); + } else { + /* We'll sign the token */ + packet = AdbProtocol.generateAuth(AdbProtocol.AUTH_TYPE_SIGNATURE, + conn.crypto.signAdbTokenPayload(msg.payload)); + conn.sentSignature = true; + } + + /* Write the AUTH reply */ + conn.outputStream.write(packet); + conn.outputStream.flush(); + } + break; + + case AdbProtocol.CMD_CNXN: + synchronized (conn) { + cmd = "CNXN"; + /* We need to store the max data size */ + conn.maxData = msg.arg1; + + /* Mark us as connected and unwait anyone waiting on the connection */ + conn.connected = true; + conn.notifyAll(); + } + break; + + default: + cmd = "default"; + /* Unrecognized packet, just drop it */ + break; + } + } catch (Exception e) { + conn.stopFlag = true; + } + } + +} diff --git a/src/AdbLib/src/com/cgutman/adblib/ByteQueueInputStream.java b/src/AdbLib/src/com/cgutman/adblib/ByteQueueInputStream.java index 194a0b1..ac54266 100644 --- a/src/AdbLib/src/com/cgutman/adblib/ByteQueueInputStream.java +++ b/src/AdbLib/src/com/cgutman/adblib/ByteQueueInputStream.java @@ -31,9 +31,7 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.LinkedBlockingQueue; /** * 参照ByteArrayInputStream实现,支持使用byte[]队列作为数据源 @@ -49,15 +47,13 @@ class ByteQueueInputStream extends InputStream { /** * 数据源 */ - protected final Queue readQueue; + protected final LinkedBlockingQueue readQueue; /** * 当前读取列表 */ private byte[] currentBytes; - private AtomicBoolean waitingAdd = new AtomicBoolean(false); - /** * The index of the next character to read from the input stream buffer. * This value should always be nonnegative @@ -110,10 +106,12 @@ class ByteQueueInputStream extends InputStream { * buf. * */ - protected ByteQueueInputStream() { - this.readQueue = new ConcurrentLinkedQueue<>(); + public ByteQueueInputStream() { + this.readQueue = new LinkedBlockingQueue<>(); this.pos = 0; this.count = 0; + + // 最大20K this.currentBytes = null; this.isRunning = true; } @@ -124,40 +122,49 @@ public void openSocketForwardingMode() { public void closeSocketForwardingMode() { this.socketForward = false; - synchronized (addLock) { - addLock.notifyAll(); - } } /** * 添加bytes到队列中 * @param bytes */ - protected void addBytes(byte[] bytes) { + public void addBytes(byte[] bytes) { + long startTime = System.currentTimeMillis(); this.readQueue.add(bytes); - - if (socketForward) { - synchronized (addLock) { - addLock.notifyAll(); - } - } } /** * 加载数据,直到当前bytes非空或者队列为空 */ private void pollToAvailable() { - while (pos >= count) { - currentBytes = this.readQueue.poll(); - - // 重设 - if (currentBytes != null) { - pos = 0; - count = currentBytes.length; - } else { - pos = 0; - count = 0; - break; + if (pos >= count) { + synchronized (lock) { + while (pos >= count) { + // 非forward模式,不强制poll + if (!socketForward) { + if (readQueue.isEmpty()) { + pos = 0; + count = 0; + return; + } + } + + try { + currentBytes = this.readQueue.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 重设 + if (currentBytes != null) { + pos = 0; + count = currentBytes.length; + } else { + pos = 0; + count = 0; + break; + } + } } } } @@ -183,29 +190,8 @@ public int read() { synchronized (addLock) { pollToAvailable(); + return (pos < count) ? currentBytes[pos++] & 0xff : -1; } - - boolean available = pos < count; - - if (!available && socketForward) { - synchronized (addLock) { - // 等待添加数据 - try { - addLock.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - -// addLock.notifyAll(); - } - } - - synchronized (lock) { - // 当位置大于等于计数(读完或者未读取) - pollToAvailable(); - } - } - - return (pos < count) ? currentBytes[pos++] & 0xff : -1; } /** * Reads up to len bytes of data into an array of bytes @@ -246,45 +232,21 @@ public int read(byte b[], int off, int len) { return -1; } - int availableCount = 0; - synchronized (lock) { - // 首先移动到可用bytes - pollToAvailable(); - availableCount = count - pos; - } - // 初始计数 - int realCount = 0; - - if (availableCount == 0) { - if (socketForward) { - try { - synchronized (addLock) { - // 等待添加数据 - addLock.wait(); - } - + int realCount = -1; - synchronized (lock) { - pollToAvailable(); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - } else { - return -1; - } - } - - synchronized (lock) { + synchronized (addLock) { // 只填充一次数据,不需要按照len填充 - if (availableCount > 0) { - int toCopy = Math.min(availableCount, len); - System.arraycopy(currentBytes, pos, b, off + realCount, toCopy); + pollToAvailable(); + + if (count - pos > 0) { + int toCopy = Math.min(count - pos, len); + System.arraycopy(currentBytes, pos, b, off, toCopy); pos += toCopy; - realCount += toCopy; + realCount = toCopy; } + return realCount; } } @@ -303,7 +265,7 @@ public int read(byte b[], int off, int len) { */ @Override public long skip(long n) { - synchronized (lock) { + synchronized (addLock) { // 首先移动到可用bytes pollToAvailable(); @@ -417,5 +379,4 @@ public void reset() { public void close() throws IOException { isRunning = false; } - } diff --git a/src/androidWebsockets/build.gradle b/src/androidWebsockets/build.gradle index 3428b09..0d9ce9e 100755 --- a/src/androidWebsockets/build.gradle +++ b/src/androidWebsockets/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 25 - buildToolsVersion "26.0.2" + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { minSdkVersion 17 - targetSdkVersion 25 + targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName "1.0" } diff --git a/src/app/build.gradle b/src/app/build.gradle index 44760c6..8c02aeb 100644 --- a/src/app/build.gradle +++ b/src/app/build.gradle @@ -13,72 +13,55 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -apply plugin: 'com.android.application' +apply plugin: 'com.android.library' android { - compileSdkVersion 25 - buildToolsVersion "26.0.2" + compileSdkVersion rootProject.ext.compileSdkVersion + buildToolsVersion rootProject.ext.buildToolsVersion defaultConfig { - applicationId "com.alipay.hulu" minSdkVersion 18 - targetSdkVersion 25 - multiDexEnabled true + targetSdkVersion rootProject.ext.targetSdkVersion } - packagingOptions { - exclude 'META-INF/LICENSE' - exclude 'META-INF/DEPENDENCIES' + lintOptions { + abortOnError false } buildTypes { release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + consumerProguardFiles 'proguard-rules.pro' } debug { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - versionNameSuffix "-" + new Date().format("yyMMddHHmm") + consumerProguardFiles 'proguard-rules.pro' } } - - - signingConfigs { - release { - v1SigningEnabled true - v2SigningEnabled true - } - } - - dexOptions { - javaMaxHeapSize "4g" - } } dependencies { - implementation 'com.android.support:support-v4:25.4.0' - implementation 'com.android.support:support-core-utils:25.4.0' - implementation 'com.android.support:appcompat-v7:25.4.0' - implementation 'com.android.support:recyclerview-v7:25.4.0' - implementation 'com.android.support:design:25.4.0' + implementation "androidx.legacy:legacy-support-v4:${ANDROIDX_SUPPORT_V4_VERSION}" + implementation "androidx.legacy:legacy-support-core-utils:${ANDROIDX_SUPPORT_CORE_UTILS_VERSION}" + implementation "androidx.appcompat:appcompat:${ANDROIDX_APPCOMPAT_VERSION}" + implementation "androidx.recyclerview:recyclerview:${ANDROIDX_RECYCLERVIEW_VERSION}" + implementation "com.google.android.material:material:${ANDROIDX_MATERIAL_VERSION}" implementation 'com.github.lecho:hellocharts-library:1.5.8@aar' - implementation 'com.alibaba:fastjson:1.1.71.android' - implementation 'org.greenrobot:greendao:3.2.2' + implementation "com.alibaba:fastjson:${FASTJSON_VERSION}" + implementation 'org.greenrobot:greendao:3.3.0' implementation 'com.squareup.okhttp3:okhttp:3.12.3' - implementation 'com.dlazaro66.qrcodereaderview:qrcodereaderview:2.0.3' - implementation 'com.liulishuo.filedownloader:library:1.7.6' + implementation 'com.liulishuo.filedownloader:library:1.7.7' + implementation 'cn.dreamtobe.filedownloader:filedownloader-okhttp3-connection:1.1.0' implementation 'com.hyman:flowlayout-lib:1.1.2' implementation 'com.yydcdut:sdlv:0.7.6' implementation 'com.atlassian.commonmark:commonmark:0.13.0' + implementation "com.google.zxing:core:3.4.0" implementation('com.theartofdev.edmodo:android-image-cropper:2.5.1') { exclude group: "com.android.support" } - implementation('com.github.bumptech.glide:glide:4.9.0') { + implementation('com.github.bumptech.glide:glide:4.11.0') { exclude group: "com.android.support" } - implementation 'commons-io:commons-io:2.6' + implementation "commons-io:commons-io:${COMMON_IO_VERSION}" implementation ('com.orhanobut:logger:2.2.0') { exclude group: "com.android.support" } - implementation 'com.android.support:multidex:1.0.3' + compileOnly "androidx.multidex:multidex:${ANDROIDX_MULTIDEX_VERSION}" implementation project(':shared') } diff --git a/src/app/proguard-rules.pro b/src/app/proguard-rules.pro index 5708bb4..1effb5d 100644 --- a/src/app/proguard-rules.pro +++ b/src/app/proguard-rules.pro @@ -25,57 +25,6 @@ #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} - -# Uncomment this to preserve the line number information for -# debugging stack traces. --keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. --renamesourcefileattribute SourceFile - -#==================================【基本配置】================================== -# 代码混淆压缩比,在0~7之间,默认为5,一般不下需要修改 --optimizationpasses 5 -# 混淆时不使用大小写混合,混淆后的类名为小写 -# windows下的同学还是加入这个选项吧(windows大小写不敏感) --dontusemixedcaseclassnames -# 指定不去忽略非公共的库的类 -# 默认跳过,有些情况下编写的代码与类库中的类在同一个包下,并且持有包中内容的引用,此时就需要加入此条声明 --dontskipnonpubliclibraryclasses -# 指定不去忽略非公共的库的类的成员 --dontskipnonpubliclibraryclassmembers -# 不做预检验,preverify是proguard的四个步骤之一 -# Android不需要preverify,去掉这一步可以加快混淆速度 --dontpreverify -# 有了verbose这句话,混淆后就会生成映射文件 --verbose -#apk 包内所有 class 的内部结构 --dump class_files.txt -#未混淆的类和成员 --printseeds seeds.txt -#列出从 apk 中删除的代码 --printusage unused.txt -#混淆前后的映射 --printmapping mapping.txt -# 指定混淆时采用的算法,后面的参数是一个过滤器 -# 这个过滤器是谷歌推荐的算法,一般不改变 --optimizations !code/simplification/artithmetic,!field/*,!class/merging/* -# 保护代码中的Annotation不被混淆 -# 这在JSON实体映射时非常重要,比如fastJson --keepattributes *Annotation* -# 避免混淆泛型 -# 这在JSON实体映射时非常重要,比如fastJson --keepattributes Signature -# 抛出异常时保留代码行号 --keepattributes SourceFile,LineNumberTable -#忽略警告 --ignorewarning -#==================================【项目配置】================================== -# 保留所有的本地native方法不被混淆 --keepclasseswithmembernames class * { -native ; -} # 保留了继承自Activity、Application这些类的子类 -keep public class * extends android.app.Activity -keep public class * extends android.app.Application @@ -89,6 +38,15 @@ native ; -keep public class com.null.test.ui.fragment.** {*;} #如果引用了v4或者v7包 -dontwarn android.support.** + +# AndroidX 方法类 +#-keep class com.google.android.material.** {*;} +#-keep class androidx.** {*;} +-keep public class * extends androidx.** +-keep interface androidx.** {*;} +-dontwarn com.google.android.material.** +-dontnote com.google.android.material.** +-dontwarn androidx.** # 保留Activity中的方法参数是view的方法, -keepclassmembers class * extends android.app.Activity { public void * (android.view.View); @@ -136,9 +94,6 @@ void *(**On*Event); #Patch相关类 -keep class com.alipay.hulu.upgrade.PatchResponse { *; } -keep class com.alipay.hulu.upgrade.PatchResponse$DataBean { *; } --keep class com.alipay.hulu.common.utils.ClassUtil$PatchVersionInfo { *; } --keep class com.alipay.hulu.common.utils.patch.PatchDescription {*;} - #内部方法 -keepattributes EnclosingMethod @@ -158,24 +113,6 @@ void *(**On*Event); # OkHttp platform used only on JVM and when Conscrypt dependency is available. -dontwarn okhttp3.internal.platform.ConscryptPlatform -# injector --keepclassmembers class ** { -@com.alipay.hulu.common.injector.param.Subscriber ; -} --keepclassmembers class ** { -@com.alipay.hulu.common.injector.provider.Provider ; -} - -# BroadcastPackage --keep class com.alipay.hulu.shared.io.socket.LocalNetworkBroadcastService$BroadcastPackage { *; } --keep enum com.alipay.hulu.shared.io.socket.enums.BroadcastCommandEnum { *; } - -# ActionProvider --keep @com.alipay.hulu.common.annotation.Enable class * - -#PrepareWorker --keep interface com.alipay.hulu.shared.node.utils.prepare.PrepareWorker { *; } --keep @com.alipay.hulu.shared.node.utils.prepare.PrepareWorker$PrepareTool class * implements com.alipay.hulu.shared.node.utils.prepare.PrepareWorker { *; } #Github Replease -keep class com.alipay.hulu.bean.GithubReleaseBean { *; } @@ -189,62 +126,9 @@ void *(**On*Event); -keep class com.android.permission.** {*;} -keep class com.codebutler.android_websockets.** {*;} -# greeendao --keep class com.alipay.hulu.shared.io.bean.** {*;} --keep class com.alipay.hulu.shared.io.db.** {*;} -### greenDAO 3 --keepclassmembers class * extends org.greenrobot.greendao.AbstractDao { -public static java.lang.String TABLENAME; -} --keep class **$Properties - -# If you do not use SQLCipher: --dontwarn org.greenrobot.greendao.database.** -# If you do not use RxJava: --dontwarn rx.** - - --keep class com.alipay.hulu.shared.node.tree.export.bean.** {*;} --keep class com.alipay.hulu.shared.node.action.OperationMethod {*;} --keep class com.alipay.hulu.shared.node.tree.OperationNode {*;} --keep class com.alipay.hulu.shared.node.tree.OperationNode$AssistantNode {*;} --keep class com.alipay.hulu.shared.node.tree.AbstractNodeTree { *; } --keep class com.alipay.hulu.shared.node.tree.FakeNodeTree { *; } --keep class com.alipay.hulu.shared.node.tree.accessibility.tree.AccessibilityNodeTree { *; } --keep class * extends com.alipay.hulu.shared.node.tree.AbstractNodeTree { *; } - --keep class com.alipay.hulu.common.bean.** {*;} - --keep interface com.alipay.hulu.common.tools.AbstCmdLine {*;} - --keep class com.alipay.hulu.common.utils.patch.PatchContext {*;} - -# Glide --keep class com.alipay.hulu.common.utils.Glide* { *; } --keep public class * implements com.bumptech.glide.module.GlideModule --keep public class * extends com.bumptech.glide.module.AppGlideModule --keep public enum com.bumptech.glide.load.ImageHeaderParser$** { - **[] $VALUES; - public *; -} - --keep class ** implements com.alipay.hulu.shared.display.items.base.Displayable { -public void clear(); -} --keep interface com.alipay.hulu.common.service.base.ExportService { *; } --keep @interface com.alipay.hulu.common.service.base.LocalService {*;} --keep class com.alipay.hulu.common.utils.patch.PatchClassLoader { -public com.alipay.hulu.common.utils.patch.PatchContext getContext(); -} --keep class ** implements com.alipay.hulu.common.service.base.ExportService { *; } - --keep interface ** extends com.alipay.hulu.common.service.base.ExportService { *; } - --dontwarn android.support.v4.** --keep class android.support.** {*;} -keepattributes Exceptions,InnerClasses,Signature -#视频直播混淆 + #fastjson -dontwarn com.alibaba.fastjson.** -keep class com.alibaba.fastjson.** { *; } diff --git a/src/app/src/main/AndroidManifest.xml b/src/app/src/main/AndroidManifest.xml index efc0b14..7728b8b 100644 --- a/src/app/src/main/AndroidManifest.xml +++ b/src/app/src/main/AndroidManifest.xml @@ -15,9 +15,7 @@ --> + package="com.alipay.hulu"> @@ -27,6 +25,9 @@ + + + @@ -48,8 +49,11 @@ @@ -57,7 +61,7 @@ android:name=".activity.SplashActivity" android:label="@string/app_name" android:logo="@drawable/solopi_main" - android:screenOrientation="portrait" + android:screenOrientation="unspecified" android:theme="@style/AppNoTitleBarTheme"> @@ -66,75 +70,79 @@ + android:label="@string/activity__performance_display" + android:screenOrientation="unspecified" /> + android:label="@string/activity__case_edit" + android:configChanges="orientation|keyboard|screenSize" + android:screenOrientation="unspecified"/> + android:label="@string/activity__about" + android:screenOrientation="unspecified"/> + android:screenOrientation="unspecified" + android:label="@string/activity__setting" /> + android:label="@string/activity__performance_test" + android:configChanges="orientation|keyboard|screenSize" + android:screenOrientation="unspecified"/> + android:label="@string/activity__replay_result" + android:configChanges="orientation|keyboard|screenSize" + android:screenOrientation="unspecified"/> + android:label="@string/activity__case_list" + android:screenOrientation="unspecified"/> + android:label="@string/activity__index" + android:configChanges="orientation|keyboard|screenSize" + android:screenOrientation="unspecified"/> + android:screenOrientation="unspecified"/> + android:label="@string/activity__performance_manage" + android:screenOrientation="unspecified" /> + android:label="@string/activity__batch_replay" + android:screenOrientation="unspecified"/> + android:label="@string/activity__batch_replay_result" + android:configChanges="orientation|keyboard|screenSize" + android:screenOrientation="unspecified"/> - + android:label="@string/activity__license" + android:screenOrientation="unspecified" /> + + + + - - - - - - - - - + + @@ -146,7 +154,7 @@ diff --git a/src/app/src/main/assets/NOTICE.html b/src/app/src/main/assets/NOTICE.html index 1229933..3cd8495 100644 --- a/src/app/src/main/assets/NOTICE.html +++ b/src/app/src/main/assets/NOTICE.html @@ -30,6 +30,7 @@ *, ::after, ::before { box-sizing: border-box; } #write h1, #write h2, #write h3, #write h4, #write h5, #write h6, #write p, #write pre { width: inherit; } #write h1, #write h2, #write h3, #write h4, #write h5, #write h6, #write p { position: relative; } +p { line-height: inherit; } h1, h2, h3, h4, h5, h6 { break-after: avoid-page; break-inside: avoid; orphans: 2; } p { orphans: 4; } h1 { font-size: 2rem; } @@ -81,13 +82,12 @@ .footnotes-area { color: rgb(136, 136, 136); margin-top: 0.714rem; padding-bottom: 0.143rem; white-space: normal; } #write .footnote-line { white-space: pre-wrap; } @media print { - body, html { border: 1px solid transparent; height: 99%; break-after: avoid-page; break-before: avoid-page; } + body, html { border: 1px solid transparent; height: 99%; break-after: avoid; break-before: avoid; } #write { margin-top: 0px; padding-top: 0px; border-color: transparent !important; } .typora-export * { -webkit-print-color-adjust: exact; } html.blink-to-pdf { font-size: 13px; } - .typora-export #write { padding-left: 32px; padding-right: 32px; padding-bottom: 0px; break-after: avoid-page; } + .typora-export #write { padding-left: 32px; padding-right: 32px; padding-bottom: 0px; break-after: avoid; } .typora-export #write::after { height: 0px; } - @page { margin: 20mm 0px; } } .footnote-line { margin-top: 0.714em; font-size: 0.7em; } a img, img a { cursor: pointer; } @@ -138,15 +138,19 @@ .MathJax_SVG .MJX-sans-serif { font-family: sans-serif; } .MathJax_SVG { display: inline; font-style: normal; font-weight: 400; line-height: normal; zoom: 90%; text-indent: 0px; text-align: left; text-transform: none; letter-spacing: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; direction: ltr; max-width: none; max-height: none; min-width: 0px; min-height: 0px; border: 0px; padding: 0px; margin: 0px; } .MathJax_SVG * { transition: none; } -.MathJax_SVG_Display svg { vertical-align: middle !important; margin-bottom: 0px !important; } +.MathJax_SVG_Display svg { vertical-align: middle !important; margin-bottom: 0px !important; margin-top: 0px !important; } .os-windows.monocolor-emoji .md-emoji { font-family: "Segoe UI Symbol", sans-serif; } .md-diagram-panel > svg { max-width: 100%; } -[lang="mermaid"] svg, [lang="flow"] svg { max-width: 100%; } +[lang="mermaid"] svg, [lang="flow"] svg { max-width: 100%; height: auto; } [lang="mermaid"] .node text { font-size: 1rem; } table tr th { border-bottom-width: 0px; } video { max-width: 100%; display: block; margin: 0px auto; } iframe { max-width: 100%; width: 100%; border: none; } .highlight td, .highlight tr { border: 0px; } +svg[id^="mermaidChart"] { line-height: 1em; } +mark { background-color: rgb(255, 255, 0); color: rgb(0, 0, 0); background-position: initial initial; background-repeat: initial initial; } +.md-html-inline .md-plain, .md-html-inline strong, mark .md-inline-math, mark strong { color: inherit; } +mark .md-meta { color: rgb(0, 0, 0); opacity: 0.3 !important; } :root { @@ -183,7 +187,7 @@ /* latin-ext */ /* latin */ html { - font-size: 16px; + font-size: 14px; } body { @@ -199,19 +203,16 @@ #write { max-width: 860px; margin: 0 auto; - padding: 20px 30px 40px 30px; - padding-top: 20px; - padding-bottom: 100px; + padding: 20px 30px 100px; } #write p { - /* text-indent: 2rem; */ line-height: 1.6rem; word-spacing: .05rem; } #write ol li { - padding-left: 0.5rem; + text-indent: 0.5rem; } #write > ul:first-child, @@ -230,7 +231,7 @@ a { color: #42b983; font-weight: 600; - padding: 0px 2px; + padding: 0 2px; text-decoration: none; } @@ -259,32 +260,32 @@ h1 tt, h1 code { - font-size: inherit; + font-size: inherit !important; } h2 tt, h2 code { - font-size: inherit; + font-size: inherit !important; } h3 tt, h3 code { - font-size: inherit; + font-size: inherit !important; } h4 tt, h4 code { - font-size: inherit; + font-size: inherit !important; } h5 tt, h5 code { - font-size: inherit; + font-size: inherit !important; } h6 tt, h6 code { - font-size: inherit; + font-size: inherit !important; } h2 a, @@ -292,18 +293,6 @@ color: #34495e; } -h3 a:before { - content: '#'; - color: #42b983; - position: absolute;; - left: -0.7em; - margin-top: -0.05em; - padding-right: 0.5em; - font-size: 1.2em; - line-height: 1; - font-weight: bold; -} - h1 { padding-bottom: .4rem; font-size: 2.2rem; @@ -313,7 +302,7 @@ h2 { font-size: 1.75rem; line-height: 1.225; - margin: 35px 0px 15px 0px; + margin: 35px 0 15px; padding-bottom: 0.5em; border-bottom: 1px solid #ddd; } @@ -321,7 +310,7 @@ h3 { font-size: 1.4rem; line-height: 1.43; - margin: 20px 0px 7px 0px; + margin: 20px 0 7px; } h4 { @@ -424,7 +413,7 @@ blockquote { border-left: 4px solid #42b983; - padding: 10px 0px 10px 15px; + padding: 10px 15px; color: #777; background-color: rgba(66, 185, 131, .1); } @@ -472,11 +461,11 @@ } #write strong { - padding: 0px 1px 0 1px; + padding: 0 1px; } #write em { - padding: 0px 5px 0 2px; + padding: 0 5px 0 2px; } #write table thead th { @@ -491,7 +480,7 @@ border: 1px solid #F4F4F4; -webkit-font-smoothing: initial; margin: 0.8rem 0 !important; - padding: 0.3rem 0rem !important; + padding: 0.3rem 0 !important; line-height: 1.43rem; background-color: #F8F8F8 !important; border-radius: 2px; @@ -508,7 +497,7 @@ margin: 0 2px; padding: 2px 4px; border-radius: 2px; - font-family: Source Sans Pro, Roboto Mono, Monaco, courier, monospace !important; + font-family: Roboto Mono, Source Sans Pro, Monaco, courier, monospace !important; font-size: 0.92rem; color: #e96900; background-color: #f8f8f8; @@ -546,13 +535,6 @@ margin-left: -1.3em; } -@media screen and (min-width: 914px) { - /*body { - width: 854px; - margin: 0 auto; - }*/ -} - @media print { html { font-size: 13px; @@ -608,10 +590,9 @@ } .md-image > .md-meta { - /*border: 1px solid #ddd;*/ border-radius: 3px; font-family: Consolas, "Liberation Mono", Courier, monospace; - padding: 2px 0px 0px 4px; + padding: 2px 0 0 4px; font-size: 0.9em; color: inherit; } @@ -664,7 +645,6 @@ } .mac-seamless-mode #typora-sidebar { - background-color: #fafafa; background-color: var(--side-bar-bg-color); } @@ -676,122 +656,136 @@ --item-hover-bg-color: #E6F0FE; } + .typora-export li, .typora-export p, .typora-export, .footnote-line {white-space: normal;} -

AdbLib (Modified)

https://github.com/cgutman/AdbLib

Copyright (c) 2013, Cameron Gutman -All rights reserved.

Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution.

Neither the name of the {organization} nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause

android-websockets (Modified)

https://github.com/codebutler/android-websockets

Copyright (c) 2009-2012 James Coglan -Copyright (c) 2012 Eric Butler

Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the 'Software'), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions:

The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

MIT: https://opensource.org/licenses/MIT

MethodInterceptProxy (Modified)

https://github.com/zhangke3016/MethodInterceptProxy

Copyright 2016 zhangke

Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable -law or agreed to in writing, software distributed under the License is distributed -on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express -or implied. See the License for the specific language governing permissions and -imitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

FloatWindowPermission (Modified)

https://github.com/zhaozepeng/FloatWindowPermission

Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved.

android-support-library

https://developer.android.com/topic/libraries/support-library

Copyright (C) 2015 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

android-asyncservice

https://github.com/JoanZapata/android-asyncservice

Copyright 2014 Joan Zapata

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

hellocharts-android

https://github.com/lecho/hellocharts-android

HelloCharts -Copyright 2014 Leszek Wach

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

fastjson

https://github.com/alibaba/fastjson

Copyright 1999-2018 Alibaba Group Holding Ltd.

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at following link.

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

greenDAO

https://github.com/greenrobot/greenDAO

Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org)

greenDAO binaries and source code can be used according to the Apache License, Version 2.0.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

QRCodeReaderView

https://github.com/dlazaro66/QRCodeReaderView

Copyright 2017 David Lázaro

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

FileDownloader

https://github.com/lingochamp/FileDownloader

Copyright (c) 2015 LingoChamp Inc.

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

glide

https://github.com/bumptech/glide

Dual licensed under either the terms of Simplified BSD License, or alternatively under the terms of The Apache Software License, Version 2.0

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause

common-io

http://commons.apache.org/proper/commons-io/

Copyright © 2003-2018 The Apache Software Foundation

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

commonmark-java

https://github.com/atlassian/commonmark-java

Copyright (c) 2015-2019 Atlassian and others.

BSD (2-clause) licensed, see LICENSE.txt file.

BSD (2-clause): https://opensource.org/licenses/BSD-2-Clause

logger

https://github.com/orhanobut/logger

Copyright 2018 Orhan Obut

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

SlideAndDragListView

https://github.com/yydcdut/SlideAndDragListView

Copyright 2015 yydcdut

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

FlowLayout

https://github.com/hongyangAndroid/FlowLayout

Copyright 2015 hongyangAndroid

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

Android-Image-Cropper

https://github.com/ArthurHub/Android-Image-Cropper

Originally forked from edmodo/cropper.

Copyright 2016, Arthur Teplitzki, 2013, Edmodo, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

cropper

https://github.com/edmodo/cropper

Copyright 2013, Edmodo, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

ScreenRecorder (Modified)

https://github.com/yrom/ScreenRecorder

Copyright (c) 2014 Yrom Wang http://www.yrom.net

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

BrokenKeyDerivation

https://android.googlesource.com/platform/development/+/master/samples/BrokenKeyDerivation?autodive=0

Copyright (C) 2007 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

OpenCV

https://opencv.org

By downloading, copying, installing or using the software you agree to this license. If you do not agree to this license, do not download, install, copy or use the software.

License Agreement -For Open Source Computer Vision Library -(3-clause BSD License)

Copyright (C) 2000-2019, Intel Corporation, all rights reserved.Copyright (C) 2009-2011, Willow Garage Inc., all rights reserved.Copyright (C) 2009-2016, NVIDIA Corporation, all rights reserved.Copyright (C) 2010-2013, Advanced Micro Devices, Inc., all rights reserved.Copyright (C) 2015-2016, OpenCV Foundation, all rights reserved.Copyright (C) 2015-2016, Itseez Inc., all rights reserved.Third party copyrights are property of their respective owners.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the names of the copyright holders nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission.

This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall copyright holders or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.

BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause

FFmpeg

https://ffmpeg.org

Copyright (C) 2016 Fabrice Bellard

This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

LGPL 2.1: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html

Minicap

https://github.com/openstf/minicap

Copyright © 2013 CyberAgent, Inc. -Copyright © 2016 The OpenSTF Project

Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

部分SVG图片来源

https://www.iconfont.cn

+

AdbLib (Modified)

https://github.com/cgutman/AdbLib

Copyright (c) 2013, Cameron Gutman +All rights reserved.

Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution.

Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause

android-websockets (Modified)

https://github.com/codebutler/android-websockets

Copyright (c) 2009-2012 James Coglan +Copyright (c) 2012 Eric Butler

Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the 'Software'), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions:

The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

MIT: https://opensource.org/licenses/MIT

MethodInterceptProxy (Modified)

https://github.com/zhangke3016/MethodInterceptProxy

Copyright 2016 zhangke

Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable +law or agreed to in writing, software distributed under the License is distributed +on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +or implied. See the License for the specific language governing permissions and +imitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

FloatWindowPermission (Modified)

https://github.com/zhaozepeng/FloatWindowPermission

Copyright (C) 2016 Facishare Technology Co., Ltd. All Rights Reserved.

android-support-library

https://developer.android.com/topic/libraries/support-library

Copyright (C) 2015 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

android-asyncservice

https://github.com/JoanZapata/android-asyncservice

Copyright 2014 Joan Zapata

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

hellocharts-android

https://github.com/lecho/hellocharts-android

HelloCharts +Copyright 2014 Leszek Wach

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

fastjson

https://github.com/alibaba/fastjson

Copyright 1999-2018 Alibaba Group Holding Ltd.

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at following link.

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

greenDAO

https://github.com/greenrobot/greenDAO

Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org)

greenDAO binaries and source code can be used according to the Apache License, Version 2.0.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

QRCodeReaderView

https://github.com/dlazaro66/QRCodeReaderView

Copyright 2017 David Lázaro

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

FileDownloader

https://github.com/lingochamp/FileDownloader

Copyright (c) 2015 LingoChamp Inc.

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

FileDownloader OkHttp3 Connection

https://github.com/Jacksgong/filedownloader-okhttp3-connection

Copyright (C) 2016 Jacksgong(blog.dreamtobe.cn)

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

glide

https://github.com/bumptech/glide

Dual licensed under either the terms of Simplified BSD License, or alternatively under the terms of The Apache Software License, Version 2.0

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause

common-io

http://commons.apache.org/proper/commons-io/

Copyright © 2003-2018 The Apache Software Foundation

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

commonmark-java

https://github.com/atlassian/commonmark-java

Copyright (c) 2015-2019 Atlassian and others.

BSD (2-clause) licensed, see LICENSE.txt file.

BSD (2-clause): https://opensource.org/licenses/BSD-2-Clause

logger

https://github.com/orhanobut/logger

Copyright 2018 Orhan Obut

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

SlideAndDragListView

https://github.com/yydcdut/SlideAndDragListView

Copyright 2015 yydcdut

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

FlowLayout

https://github.com/hongyangAndroid/FlowLayout

Copyright 2015 hongyangAndroid

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

Android-Image-Cropper

https://github.com/ArthurHub/Android-Image-Cropper

Originally forked from edmodo/cropper.

Copyright 2016, Arthur Teplitzki, 2013, Edmodo, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

cropper

https://github.com/edmodo/cropper

Copyright 2013, Edmodo, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License. You may obtain a copy of the License in the LICENSE file, or at:

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

ScreenRecorder (Modified)

https://github.com/yrom/ScreenRecorder

Copyright (c) 2014 Yrom Wang http://www.yrom.net

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

BrokenKeyDerivation

https://android.googlesource.com/platform/development/+/master/samples/BrokenKeyDerivation?autodive=0

Copyright (C) 2007 The Android Open Source Project

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

OpenCV

https://opencv.org

By downloading, copying, installing or using the software you agree to this license. If you do not agree to this license, do not download, install, copy or use the software.

License Agreement +For Open Source Computer Vision Library +(3-clause BSD License)

Copyright (C) 2000-2019, Intel Corporation, all rights reserved.Copyright (C) 2009-2011, Willow Garage Inc., all rights reserved.Copyright (C) 2009-2016, NVIDIA Corporation, all rights reserved.Copyright (C) 2010-2013, Advanced Micro Devices, Inc., all rights reserved.Copyright (C) 2015-2016, OpenCV Foundation, all rights reserved.Copyright (C) 2015-2016, Itseez Inc., all rights reserved.Third party copyrights are property of their respective owners.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  • Neither the names of the copyright holders nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission.

This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall copyright holders or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.

BSD-3-Clause: https://opensource.org/licenses/BSD-3-Clause

FFmpeg

https://ffmpeg.org

Copyright (C) 2016 Fabrice Bellard

This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

LGPL 2.1: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html

Minicap

https://github.com/openstf/minicap

Copyright © 2013 CyberAgent, Inc. +Copyright © 2016 The OpenSTF Project

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

scrcpy (Modified)

https://github.com/Genymobile/scrcpy

Copyright (C) 2018 Genymobile +Copyright (C) 2018-2019 Romain Vimont

Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License.

Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0

部分SVG图片来源

https://www.iconfont.cn

\ No newline at end of file diff --git a/src/app/src/main/ic_launcher-playstore.png b/src/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..3a7151a Binary files /dev/null and b/src/app/src/main/ic_launcher-playstore.png differ diff --git a/src/app/src/main/java/com/alipay/hulu/actions/ImageCompareActionProvider.java b/src/app/src/main/java/com/alipay/hulu/actions/ImageCompareActionProvider.java index aae5143..b701f0a 100644 --- a/src/app/src/main/java/com/alipay/hulu/actions/ImageCompareActionProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/actions/ImageCompareActionProvider.java @@ -85,9 +85,6 @@ public void onDestroy(Context context) { @Override public boolean canProcess(String action) { - if (Build.VERSION.SDK_INT < 21) { - return false; - } return StringUtil.equals(action, ACTION_CLICK_BY_SCREENSHOT) || StringUtil.equals(action, ACTION_ASSERT_SCREENSHOT); @@ -97,10 +94,6 @@ public boolean canProcess(String action) { public boolean processAction(final String targetAction, AbstractNodeTree node, OperationMethod method, final OperationContext context) { - if (Build.VERSION.SDK_INT < 21) { - return false; - } - // 同步执行,没点到就中断 if (StringUtil.equals(targetAction, ACTION_CLICK_BY_SCREENSHOT)) { String base64 = method.getParam(KEY_TARGET_IMAGE); @@ -164,7 +157,7 @@ public void run() { }, 1000); // 执行adb命令 - CmdTools.execAdbCmd("input tap " + target.centerX() + " " + target.centerY(), 2000); + context.executor.executeClick(target.centerX(), target.centerY()); // 等500ms MiscUtil.sleep(500); @@ -218,7 +211,7 @@ public void run() { // 还没有,无法执行 if (rs == null) { - LauncherApplication.getInstance().showToast("图像断言失败"); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.image_compare__assert_failed)); return false; } } @@ -228,7 +221,7 @@ public void run() { Rect target = findTargetRect(rs, query, context.screenWidth, context.screenHeight, defaultWidth); if (target == null) { LogUtil.e(TAG, "Can't find target Image"); - LauncherApplication.getInstance().showToast("图像断言失败"); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.image_compare__assert_failed)); return false; } else { // 高亮控件 @@ -242,7 +235,7 @@ public void run() { // 执行adb命令 context.notifyOperationFinish(); - LauncherApplication.getInstance().showToast("图像断言成功"); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.image_compare__assert_success)); return true; } } catch (Exception e) { @@ -250,7 +243,7 @@ public void run() { context.notifyOperationFinish(); } - LauncherApplication.getInstance().showToast("图像断言失败"); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.image_compare__assert_failed)); return false; } @@ -261,13 +254,9 @@ public void run() { public Map provideActions(AbstractNodeTree node) { Map actionMap = new HashMap<>(2); - if (Build.VERSION.SDK_INT < 21) { - return actionMap; - } - // 配置功能项 - actionMap.put(ACTION_ASSERT_SCREENSHOT, "截图断言"); - actionMap.put(ACTION_CLICK_BY_SCREENSHOT, "根据截图点击"); + actionMap.put(ACTION_ASSERT_SCREENSHOT, StringUtil.getString(R.string.image_compare__screenshot_assert)); + actionMap.put(ACTION_CLICK_BY_SCREENSHOT, StringUtil.getString(R.string.image_compare__screenshot_click)); return actionMap; } @@ -275,11 +264,6 @@ public Map provideActions(AbstractNodeTree node) { @Override public void provideView(final Context context, String action, final OperationMethod method, final AbstractNodeTree node, final ViewLoadCallback callback) { - if (Build.VERSION.SDK_INT < 21) { - LogUtil.e(TAG, "不支持android: " + Build.VERSION.SDK_INT); - callback.onViewLoaded(null); - return; - } if (!StringUtil.equals(action, ACTION_CLICK_BY_SCREENSHOT) && !StringUtil.equals(action, ACTION_ASSERT_SCREENSHOT)) { diff --git a/src/app/src/main/java/com/alipay/hulu/actions/PerformanceActionProvider.java b/src/app/src/main/java/com/alipay/hulu/actions/PerformanceActionProvider.java index bf104f6..73e4471 100644 --- a/src/app/src/main/java/com/alipay/hulu/actions/PerformanceActionProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/actions/PerformanceActionProvider.java @@ -113,10 +113,10 @@ public void run() { File folder = RecordUtil.saveToFile(records); // 显示提示框 - LauncherApplication.getInstance().showToast("录制数据已经保存到\"" + folder.getPath() + "\"下"); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.performance__record_save, folder.getPath())); } else { String response = RecordUtil.uploadData(uploadUrl, records); - LauncherApplication.getInstance().showToast("录制数据已经上传至\"" + uploadUrl + "\",响应结果: " + response); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.performance__record_upload, uploadUrl, response)); } } }); @@ -135,9 +135,9 @@ public Map provideActions(AbstractNodeTree node) { // 配置功能项 if (isRecording) { - actionMap.put(ACTION_STOP_RECORD, "停止性能录制"); + actionMap.put(ACTION_STOP_RECORD, StringUtil.getString(R.string.performance__stop_record)); } else { - actionMap.put(ACTION_START_RECORD, "开始性能录制"); + actionMap.put(ACTION_START_RECORD, StringUtil.getString(R.string.performance__start_record)); } return actionMap; @@ -202,7 +202,7 @@ public void onClick(View v) { itemView.setOnCheckedChangeListener(listener); // 暂存下 - itemView.setTag(info.getName()); + itemView.setTag(info.getKey()); // 添加子节点 linearLayout.addView(itemView, params); diff --git a/src/app/src/main/java/com/alipay/hulu/actions/RecordScreenActionProvider.java b/src/app/src/main/java/com/alipay/hulu/actions/RecordScreenActionProvider.java index 1dc7a79..e5ae545 100644 --- a/src/app/src/main/java/com/alipay/hulu/actions/RecordScreenActionProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/actions/RecordScreenActionProvider.java @@ -135,7 +135,7 @@ public boolean processAction(String targetAction, AbstractNodeTree node, final O uploadUrl = method.getParam(KEY_RECORD_UPLOAD_URL); if (ClassUtil.getPatchInfo(VideoAnalyzer.SCREEN_RECORD_PATCH) == null) { - LauncherApplication.getInstance().showToast("加载计算插件中"); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.settings__load_plugin)); context.notifyOnFinish(new Runnable() { @Override public void run() { @@ -176,7 +176,7 @@ public void run() { @Override public void run() { try { - injectorService.pushMessage(SHOW_LOADING_DIALOG, "正在计算响应耗时"); + injectorService.pushMessage(SHOW_LOADING_DIALOG, StringUtil.getString(R.string.record_screen__calculating_response_time)); long startTime = binder.stopRecord(); @@ -233,9 +233,9 @@ public Map provideActions(AbstractNodeTree node) { Map desc = new HashMap<>(2); if (!isRecording) { - desc.put(ACTION_START_RECORD_SCREEN, "开始响应耗时计算"); + desc.put(ACTION_START_RECORD_SCREEN, StringUtil.getString(R.string.record_screen__start_launch_time)); } else { - desc.put(ACTION_STOP_RECORD_SCREEN, "结束响应耗时计算"); + desc.put(ACTION_STOP_RECORD_SCREEN, StringUtil.getString(R.string.record_screen__stop_launch_time)); } return desc; @@ -252,8 +252,8 @@ private void processVideo(String path, long videoStartTime) { public void onAnalyzeFinished(final long result) { UIOperationMessage message = new UIOperationMessage(); message.eventType = UIOperationMessage.TYPE_DIALOG; - message.params.put("msg", "耗时:" + result + "ms"); - message.params.put("title", "响应耗时"); + message.params.put("msg", StringUtil.getString(R.string.record_screen__cost_time, result)); + message.params.put("title", StringUtil.getString(R.string.record_screen__response_time)); injectorService.pushMessage(null, message, false); // 如果有配置上传信息 @@ -408,7 +408,7 @@ public void onReceiveEvent(PerformActionEnum actionEnum) { LogUtil.d(TAG, "Receive event: " + actionEnum); - // 主机模式需要监控葫芦点击事件 + // 主机模式需要监控点击事件 if (inMasterMode) { LogUtil.d(TAG, "主机模式,控制悬浮窗点击"); injectorService.pushMessage("FloatClickMethod", new Callable() { diff --git a/src/app/src/main/java/com/alipay/hulu/activity/BaseActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/BaseActivity.java index 166263b..f1c2c97 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/BaseActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/BaseActivity.java @@ -16,19 +16,31 @@ package com.alipay.hulu.activity; import android.app.ProgressDialog; +import android.app.Service; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v7.app.AppCompatActivity; +import android.os.Looper; import android.text.TextUtils; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + import com.alipay.hulu.R; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.utils.ClassUtil; +import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.DeviceInfoUtil; import com.alipay.hulu.common.utils.LogUtil; @@ -59,7 +71,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { // 主线程等待 LauncherApplication.getInstance().prepareInMain(); - LogUtil.w("BaseActivity", "Activity: %s, 等待Launcher初始化耗时: %dms", getClass().getSimpleName(), System.currentTimeMillis() - startTime); + LogUtil.w("BaseActivity", "Activity: %s, waiting launcher to initialize: %dms", getClass().getSimpleName(), System.currentTimeMillis() - startTime); } // 为了正常初始化 @@ -73,6 +85,19 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } } + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(newBase); + ContextUtil.updateResources(this); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ContextUtil.updateResources(this); + } + + @Override protected void onResume() { super.onResume(); @@ -107,6 +132,20 @@ public void showInputMethod() { imManager.toggleSoftInput(0, InputMethodManager.SHOW_FORCED); } + @Override + public ComponentName startService(Intent service) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && service.getComponent() != null) { + String className = service.getComponent().getClassName(); + Class clazz = ClassUtil.getClassByName(className); + if (Service.class.isAssignableFrom(clazz)) { + if (LauncherApplication.getInstance().isServiceForeGround((Class) clazz)) { + return super.startForegroundService(service); + } + } + } + return super.startService(service); + } + //隐藏输入法 public void hideSoftInputMethod() { View view = getWindow().peekDecorView(); @@ -116,6 +155,22 @@ public void hideSoftInputMethod() { } } + /** + * 短toast + * @param stringRes + */ + public void toastShort(@StringRes final int stringRes) { + toastShort(getString(stringRes)); + } + + /** + * 短toast + * @param stringRes + */ + public void toastShort(@StringRes final int stringRes, final Object... args) { + toastShort(getString(stringRes, args)); + } + /** * toast短时间提示 * @@ -128,11 +183,10 @@ public void toastShort(final String msg) { runOnUiThread(new Runnable() { @Override public void run() { - if (toast == null) { - toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_SHORT); - } else { - toast.setText(msg); + if (toast != null) { + toast.cancel(); } + toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_SHORT); toast.show(); } }); @@ -143,6 +197,22 @@ public void toastShort(String msg, Object... args) { toastShort(formatMsg); } + /** + * 短toast + * @param stringRes + */ + public void toastLong(@StringRes final int stringRes) { + toastLong(getString(stringRes)); + } + + /** + * 短toast + * @param stringRes + */ + public void toastLong(@StringRes final int stringRes, final Object... args) { + toastLong(getString(stringRes, args)); + } + /** * toast长时间提示 * @@ -155,21 +225,15 @@ public void toastLong(final String msg) { runOnUiThread(new Runnable() { @Override public void run() { - if (toast == null) { - toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_LONG); - } else { - toast.setText(msg); + if (toast != null) { + toast.cancel(); } + toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_LONG); toast.show(); } }); } - public void toastLong(String msg, Object... args) { - String formatMsg = String.format(msg, args); - toastLong(formatMsg); - } - public void showProgressDialog(final String str) { runOnUiThread(new Runnable() { public void run() { @@ -191,8 +255,12 @@ public void run() { public void dismissProgressDialog() { runOnUiThread(new Runnable() { public void run() { - if (progressDialog != null) { - progressDialog.dismiss(); + if (progressDialog != null && progressDialog.isShowing()) { + try { + progressDialog.dismiss(); + } catch (Exception e) { + LogUtil.w(getClass().getSimpleName(), "Remove progress dialog throw exception", e); + } } } }); @@ -220,6 +288,20 @@ private void getScreenSizeInfo() { getWindowManager().getDefaultDisplay().getMetrics(DeviceInfoUtil.metrics); } + @Override + public void startActivity(final Intent intent) { + if (Thread.currentThread() != Looper.getMainLooper().getThread()) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + BaseActivity.super.startActivity(intent); + } + }); + } else { + super.startActivity(intent); + } + } + /** * 添加Fragment tag信息 * @param tag @@ -245,4 +327,8 @@ public Fragment getFragmentByTag(String tag) { return null; } + + protected T _findViewById(@IdRes int resId) { + return (T) findViewById(resId); + } } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/BatchExecutionActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/BatchExecutionActivity.java index adcc068..a5035e4 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/BatchExecutionActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/BatchExecutionActivity.java @@ -17,12 +17,12 @@ import android.os.Bundle; import android.provider.Settings; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; +import androidx.annotation.Nullable; +import com.google.android.material.tabs.TabLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; @@ -31,14 +31,15 @@ import com.alipay.hulu.R; import com.alipay.hulu.adapter.BatchExecutionListAdapter; -import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.fragment.BatchExecutionFragment; -import com.alipay.hulu.replay.BatchStepProvider; -import com.alipay.hulu.service.CaseReplayManager; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.node.utils.AppUtil; import com.alipay.hulu.ui.HeadControlPanel; +import com.alipay.hulu.util.CaseReplayUtil; import com.zhy.view.flowlayout.FlowLayout; import com.zhy.view.flowlayout.TagAdapter; import com.zhy.view.flowlayout.TagFlowLayout; @@ -72,7 +73,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { mPager = (ViewPager) findViewById(R.id.pager); mTabLayout = (TabLayout) findViewById(R.id.tab_layout); mHeadPanel = (HeadControlPanel) findViewById(R.id.head_replay_list); - mHeadPanel.setMiddleTitle("批量回放"); + mHeadPanel.setMiddleTitle(getString(R.string.activity__batch_replay)); mHeadPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -118,7 +119,7 @@ public View getView(FlowLayout parent, int position, RecordCaseInfo o) { @Override public void onClick(View v) { if (currentCases.size() == 0) { - toastShort("请选择用例"); + toastShort(getString(R.string.batch__select_case)); return; } @@ -126,9 +127,8 @@ public void onClick(View v) { @Override public void onPermissionResult(boolean result, String reason) { if (result) { - BatchStepProvider provider = new BatchStepProvider(new ArrayList<>(currentCases), mRestartApp.isChecked()); - CaseReplayManager manager = LauncherApplication.getInstance().findServiceByName(CaseReplayManager.class.getName()); - manager.start(provider, MyApplication.MULTI_REPLAY_LISTENER); + CaseReplayUtil.startReplayMultiCase(currentCases, mRestartApp.isChecked()); + startApp(currentCases.get(0).getTargetAppPackage()); } } }; @@ -155,6 +155,23 @@ public boolean onTagClick(View view, int position, FlowLayout parent) { return false; } + private void startApp(final String packageName) { + if (packageName == null) { + return; + } + + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + AppUtil.forceStopApp(packageName); + + LogUtil.e("NewRecordActivity", "强制终止应用:" + packageName); + MiscUtil.sleep(500); + AppUtil.startApp(packageName); + } + }); + } + /** * 检察权限 * @param callback diff --git a/src/app/src/main/java/com/alipay/hulu/activity/BatchReplayResultActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/BatchReplayResultActivity.java index fd4f181..38d2bbf 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/BatchReplayResultActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/BatchReplayResultActivity.java @@ -18,7 +18,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -120,7 +120,7 @@ public void onClick(View v) { finish(); } }); - mPanel.setMiddleTitle("批量回放执行结果"); + mPanel.setMiddleTitle(getString(R.string.activity__batch_replay_result)); } private static class ResultAdapter extends BaseAdapter { @@ -166,10 +166,10 @@ public View getView(int position, View convertView, ViewGroup parent) { if (bean != null) { holder.caseName.setText(bean.getCaseName()); if (TextUtils.isEmpty(bean.getExceptionMessage())) { - holder.result.setText("成功"); + holder.result.setText(R.string.constant__success); holder.result.setTextColor(0xff65c0ba); } else { - holder.result.setText("失败"); + holder.result.setText(R.string.constant__fail); holder.result.setTextColor(0xfff76262); } } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/CaseEditActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/CaseEditActivity.java index b8563dd..357023f 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/CaseEditActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/CaseEditActivity.java @@ -17,12 +17,12 @@ import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; +import androidx.annotation.Nullable; +import com.google.android.material.tabs.TabLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; import android.view.View; import com.alipay.hulu.R; @@ -32,6 +32,7 @@ import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.MiscUtil; +import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.fragment.CaseDescEditFragment; import com.alipay.hulu.fragment.CaseStepEditFragment; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; @@ -97,7 +98,7 @@ private void initData() { saved = false; caseSaveListeners.clear(); - mHeadPanel.setMiddleTitle("用例编辑"); + mHeadPanel.setMiddleTitle(getString(R.string.activity__case_edit)); mHeadPanel.setBackIconClickListener(new View.OnClickListener() { @Override @@ -132,13 +133,13 @@ public void run() { @Override public void onBackPressed() { if (shouldSave && !saved) { - LauncherApplication.getInstance().showDialog(this, "是否保存用例", "是", new Runnable() { + LauncherApplication.getInstance().showDialog(this, getString(R.string.case_edit__should_save_case), getString(R.string.constant__yes), new Runnable() { @Override public void run() { updateLocalCase(); finish(); } - }, "否", new Runnable() { + }, getString(R.string.constant__no), new Runnable() { @Override public void run() { finish(); @@ -152,7 +153,7 @@ public void run() { /** * 包装用例信息 */ - private void wrapRecordCase() { + public void wrapRecordCase() { for (WeakReference listenerRef: caseSaveListeners) { if (listenerRef.get() != null) { listenerRef.get().onCaseSave(); @@ -169,7 +170,7 @@ private void updateLocalCase() { public void run() { wrapRecordCase(); GreenDaoManager.getInstance().getRecordCaseInfoDao().save(mRecordCase); - toastShort("更新成功"); + toastShort(getString(R.string.case__update_success)); InjectorService.g().pushMessage(NewRecordActivity.NEED_REFRESH_LOCAL_CASES_LIST); saved = true; } @@ -185,6 +186,10 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } } + public RecordCaseInfo getRecordCase() { + return mRecordCase; + } + private static class CustomPagerAdapter extends FragmentPagerAdapter { private RecordCaseInfo caseInfo; private WeakReference ref; @@ -216,7 +221,7 @@ public Fragment getItem(int position) { @Override public CharSequence getPageTitle(int position) { - return position == 1? "用例信息": "用例步骤"; + return position == 1? StringUtil.getString(R.string.case_edit__info): StringUtil.getString(R.string.case_edit__steps); } @Override public int getCount() { diff --git a/src/app/src/main/java/com/alipay/hulu/activity/CaseParamEditActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/CaseParamEditActivity.java new file mode 100644 index 0000000..e640dff --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/activity/CaseParamEditActivity.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.activity; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.android.material.tabs.TabLayout; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.alibaba.fastjson.JSON; +import com.alipay.hulu.R; +import com.alipay.hulu.bean.AdvanceCaseSetting; +import com.alipay.hulu.bean.CaseRunningParam; +import com.alipay.hulu.bean.CaseStepHolder; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.MiscUtil; +import com.alipay.hulu.fragment.BaseFragment; +import com.alipay.hulu.fragment.CaseParamSeparateFragment; +import com.alipay.hulu.fragment.CaseParamUnionFragment; +import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.io.db.GreenDaoManager; +import com.alipay.hulu.ui.HeadControlPanel; + +/** + * Created by qiaoruikai on 2019-08-19 21:16. + */ +public class CaseParamEditActivity extends BaseActivity { + public static final String RECORD_CASE_EXTRA = "record_case"; + private static final String TAG = CaseParamEditActivity.class.getSimpleName(); + + // display + private TextView mCaseName; + private TextView mCaseDesc; + + private HeadControlPanel mHead; + private ViewPager mPager; + private TabLayout mTabLayout; + private CaseParamFragmentAdapter mParamAdapter; + + private RecordCaseInfo mRecordCase; + private AdvanceCaseSetting mSettings; + + private boolean saved = false; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_case_param_edit); + + initView(); + initData(); + } + + /** + * 渲染数据 + */ + private void initData() { + int caseId = getIntent().getIntExtra(RECORD_CASE_EXTRA, 0); + mRecordCase = CaseStepHolder.getCase(caseId); + + // 如果Intent中没有 + if (mRecordCase == null) { + LogUtil.e(TAG, "There is no record case"); + return; + } + + mSettings = JSON.parseObject(mRecordCase.getAdvanceSettings(), AdvanceCaseSetting.class); + + mCaseName.setText(mRecordCase.getCaseName()); + mCaseDesc.setText(getString(R.string.case_param_edit__case_desc, mRecordCase.getCaseDesc())); + + mHead.setMiddleTitle(getString(R.string.activity__gen_param_case)); + mHead.setBackIconClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onBackPressed(); + } + }); + + // 自己的用例 + mHead.setInfoIconClickListener(R.drawable.icon_save, new View.OnClickListener() { + @Override + public void onClick(View v) { + saveCase(); + } + }); + + mPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTabLayout)); + mTabLayout.setupWithViewPager(mPager); + mTabLayout.setTabGravity(TabLayout.GRAVITY_FILL); + mTabLayout.setTabMode(TabLayout.MODE_FIXED); + mTabLayout.setSelectedTabIndicatorColor(getResources().getColor(R.color.mainBlue)); + mTabLayout.post(new Runnable() { + @Override + public void run() { + MiscUtil.setIndicator(mTabLayout, 0, 0); + } + }); + + mParamAdapter = new CaseParamFragmentAdapter(getSupportFragmentManager(), mSettings); + mPager.setAdapter(mParamAdapter); + } + + /** + * 初始化界面 + */ + private void initView() { + mHead = (HeadControlPanel) findViewById(R.id.head_layout); + + mCaseName = (TextView) findViewById(R.id.case_param_edit_name); + mCaseDesc = (TextView) findViewById(R.id.case_param_edit_desc); + + mPager = (ViewPager) findViewById(R.id.case_param_pager); + mTabLayout = (TabLayout) findViewById(R.id.case_param_tab_layout); + } + + @Override + public void onBackPressed() { + if (!saved) { + LauncherApplication.getInstance().showDialog(this, getString(R.string.case_edit__should_save_case), getString(R.string.constant__yes), new Runnable() { + @Override + public void run() { + saveCase(); + finish(); + } + }, getString(R.string.constant__no), new Runnable() { + @Override + public void run() { + finish(); + } + }); + } else { + finish(); + } + } + + /** + * 保存用例 + */ + private void saveCase() { + CaseRunningParam param = mParamAdapter.getCurrentFragment().getRunningParam(); + mSettings.setRunningParam(param); + mRecordCase.setAdvanceSettings(JSON.toJSONString(mSettings)); + + updateLocalCase(); + } + + /** + * 更新本地用例 + */ + private void updateLocalCase() { + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + GreenDaoManager.getInstance().getRecordCaseInfoDao().save(mRecordCase); + toastShort(getString(R.string.case__update_success)); + InjectorService.g().pushMessage(NewRecordActivity.NEED_REFRESH_LOCAL_CASES_LIST); + saved = true; + } + }); + } + + public static class CaseParamFragmentAdapter extends FragmentPagerAdapter { + private AdvanceCaseSetting advanceCaseSetting; + private CaseParamFragment mCurrentFragment; + + public CaseParamFragmentAdapter(FragmentManager fm, AdvanceCaseSetting advanceCaseSetting) { + super(fm); + this.advanceCaseSetting = advanceCaseSetting; + } + + @Override + public int getCount() { + return 2; + } + + @Override + public Fragment getItem(int position) { + CaseParamFragment fragment; + if (position == 0) { + fragment = new CaseParamSeparateFragment(); + } else { + fragment = new CaseParamUnionFragment(); + } + fragment.setAdvanceCaseSetting(advanceCaseSetting); + return fragment; + } + + @Override + public CharSequence getPageTitle(int position) { + if (position == 0) { + return "独立模式"; + } else { + return "联合模式"; + } + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + this.mCurrentFragment = (CaseParamFragment) object; + super.setPrimaryItem(container, position, object); + } + + public CaseParamFragment getCurrentFragment() { + return mCurrentFragment; + } + } + + public static abstract class CaseParamFragment extends BaseFragment { + public abstract void setAdvanceCaseSetting(@NonNull AdvanceCaseSetting advanceCaseSetting); + public abstract CaseRunningParam getRunningParam(); + } +} \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/activity/CaseReplayResultActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/CaseReplayResultActivity.java index f374300..c2dadce 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/CaseReplayResultActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/CaseReplayResultActivity.java @@ -18,12 +18,6 @@ import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; import android.text.SpannableString; import android.text.Spanned; import android.text.style.ForegroundColorSpan; @@ -32,11 +26,18 @@ import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; + import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.R; import com.alipay.hulu.bean.CaseStepHolder; import com.alipay.hulu.bean.ReplayResultBean; +import com.alipay.hulu.bean.ScreenshotBean; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.FileUtils; @@ -47,14 +48,17 @@ import com.alipay.hulu.fragment.ReplayScreenShotFragment; import com.alipay.hulu.fragment.ReplayStepFragment; import com.alipay.hulu.ui.HeadControlPanel; -import com.alipay.hulu.util.DialogUtils; +import com.google.android.material.tabs.TabLayout; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.Field; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; +import java.util.Map; public class CaseReplayResultActivity extends BaseActivity { private static final String TAG = "CaseActivity"; @@ -85,14 +89,14 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { private void initView() { setContentView(R.layout.activity_display_replay_result); - mPager = (ViewPager) findViewById(R.id.pager); - mTabLayout = (TabLayout) findViewById(R.id.tab_layout); - mHeadPanel = (HeadControlPanel) findViewById(R.id.head_replay_result); - mCaseName = (TextView) findViewById(R.id.case_name); - mTargetApp = (TextView) findViewById(R.id.target_app); - mStartTime = (TextView) findViewById(R.id.start_time); - mEndTime = (TextView) findViewById(R.id.end_time); - mStatus = (TextView) findViewById(R.id.case_status); + mPager = findViewById(R.id.pager); + mTabLayout = findViewById(R.id.tab_layout); + mHeadPanel = findViewById(R.id.head_replay_result); + mCaseName = findViewById(R.id.case_name); + mTargetApp = findViewById(R.id.target_app); + mStartTime = findViewById(R.id.start_time); + mEndTime = findViewById(R.id.end_time); + mStatus = findViewById(R.id.case_status); } private void initData() { @@ -103,7 +107,7 @@ private void initData() { return; } - mHeadPanel.setMiddleTitle("回放结果"); + mHeadPanel.setMiddleTitle(getString(R.string.activity__replay_result)); mHeadPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -118,7 +122,7 @@ public void onClick(View v) { mHeadPanel.setInfoIconClickListener(R.drawable.icon_save, new View.OnClickListener() { @Override public void onClick(View v) { - showProgressDialog("保存中"); + showProgressDialog(getString(R.string.case_replay__saving)); BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -143,22 +147,28 @@ public void run() { mTabLayout.setTabGravity(TabLayout.GRAVITY_FILL); mTabLayout.setTabMode(TabLayout.MODE_FIXED); mTabLayout.setSelectedTabIndicatorColor(getResources().getColor(R.color.mainBlue)); - mTabLayout.post(new Runnable() { - @Override - public void run() { - setIndicator(mTabLayout, 0, 0); - } - }); +// mTabLayout.post(new Runnable() { +// @Override +// public void run() { +// setIndicator(mTabLayout, 0, 0); +// } +// }); mCaseName.setText(getString(R.string.case_replay_result__case_name, result.getCaseName())); - mTargetApp.setText(getString(R.string.case_replay_result__targe_app, result.getTargetApp())); + String targetApp = getString(R.string.case_replay_result__targe_app, result.getTargetApp()); + if (!StringUtil.isEmpty(result.getTargetAppVersion())) { + targetApp += " (" + result.getTargetAppVersion() + ")"; + } + mTargetApp.setText(targetApp); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA); mStartTime.setText(getString(R.string.case_replay_result__start_time, format.format(result.getStartTime()))); mEndTime.setText(getString(R.string.case_replay_result__end_time, format.format(result.getEndTime()))); try { - SpannableString textSpanned1 = new SpannableString(getString(R.string.case_replay_result__running_result, result.getExceptionMessage() != null? "失败" : "成功")); - textSpanned1.setSpan(new ForegroundColorSpan(result.getExceptionMessage() != null ? 0xfff76262 : 0xff65c0ba), 5, 7, Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + String status = getString(result.getExceptionMessage() != null? R.string.constant__fail : R.string.constant__success); + String displayContent = getString(R.string.case_replay_result__running_result, status); + SpannableString textSpanned1 = new SpannableString(displayContent); + textSpanned1.setSpan(new ForegroundColorSpan(result.getExceptionMessage() != null ? 0xfff76262 : 0xff65c0ba), displayContent.length() - status.length(), displayContent.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); mStatus.setText(textSpanned1); } catch (Exception e) { LogUtil.e(TAG, e.getMessage(), e); @@ -184,10 +194,44 @@ private File saveReplayResult() { JSONObject infoObj = new JSONObject(); infoObj.put("caseName", result.getCaseName()); infoObj.put("targetApp", result.getTargetApp()); + infoObj.put("targetAppPkg", result.getTargetAppPkg()); + infoObj.put("targetAppVersion", result.getTargetAppVersion()); infoObj.put("startTime", result.getStartTime()); infoObj.put("endTime", result.getEndTime()); infoObj.put("exceptionMessage", result.getExceptionMessage()); infoObj.put("exceptionStep", result.getExceptionStep()); + infoObj.put("exceptionStepId", result.getExceptionStepId()); + infoObj.put("platform", result.getPlatform()); + infoObj.put("platformVersion", result.getPlatformVersion()); + + // 截图保存 + Map screenshotFiles = result.getScreenshotFiles(); + if (screenshotFiles != null) { + List screenshots = new ArrayList<>(); + File screenshotDir = FileUtils.getSubDir("screenshots"); + + // 组装各项 + for (Map.Entry entry : screenshotFiles.entrySet()) { + File targetFile = new File(screenshotDir, entry.getValue() + ".png"); + if (targetFile.exists()) { + File copyTo = new File(root, entry.getValue() + ".png"); + try { + FileUtils.copyFile(targetFile, copyTo); + + // 记录拷贝成功的截图信息 + ScreenshotBean bean = new ScreenshotBean(); + bean.setName(entry.getKey()); + bean.setFile(copyTo.getName()); + screenshots.add(bean); + } catch (IOException e) { + LogUtil.e(TAG, "拷贝截图文件失败", e); + } + } + } + + infoObj.put("screenshots", screenshots); + } + try { JSON.writeJSONStringTo(infoObj, new FileWriter(info)); } catch (IOException e) { @@ -208,7 +252,17 @@ private File saveReplayResult() { LogUtil.e(TAG, "输出步骤信息失败", e); } + if (result.getDeviceInfo() != null) { + File deviceFile = new File(root, "device.json"); + try { + JSON.writeJSONString(new FileWriter(deviceFile), result.getDeviceInfo()); + } catch (IOException e) { + LogUtil.e(TAG, "输出设备信息失败", e); + } + } + File actionsFile = new File(root, "actions.json"); + try { JSON.writeJSONStringTo(result.getActionLogs(), new FileWriter(actionsFile)); } catch (IOException e) { @@ -290,4 +344,6 @@ public CharSequence getPageTitle(int position) { return ""; } } + + } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java index c81ad6a..42c954f 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java @@ -15,6 +15,7 @@ */ package com.alipay.hulu.activity; +import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; @@ -22,8 +23,8 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.content.FileProvider; +import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -43,12 +44,15 @@ import com.alipay.hulu.common.constant.Constant; import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.tools.CmdTools; +import com.alipay.hulu.common.trigger.Trigger; import com.alipay.hulu.common.utils.ClassUtil; import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.event.ScanSuccessEvent; import com.alipay.hulu.ui.ColorFilterRelativeLayout; import com.alipay.hulu.ui.HeadControlPanel; import com.alipay.hulu.upgrade.PatchRequest; @@ -63,6 +67,7 @@ import java.io.File; import java.io.FileFilter; import java.io.FilenameFilter; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -93,8 +98,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { initData(); loadOthers(); +// SPService.putBoolean(SPService.KEY_USE_EASY_MODE, true); + // check update - if (SPService.getBoolean(SPService.KEY_CHECK_UPDATE, true)) { + if (SPService.getBoolean(SPService.KEY_SHOULD_UPDATE_IN_APP, true) && SPService.getBoolean(SPService.KEY_CHECK_UPDATE, true)) { BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -121,7 +128,7 @@ public void onNewUpdate(final GithubReleaseBean release) { "margin-left:30px;" + "margin-top:30px;" + "font-size:" + px + "px;" + - "word-wrap:break-word;"+ + "word-wrap:break-word;" + "}" + ""; final String content = css + renderer.render(document) + ""; @@ -135,16 +142,16 @@ public void run() { webSettings.setLoadWithOverviewMode(true); webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS); webView.loadData(content, null, null); - new AlertDialog.Builder(IndexActivity.this).setTitle("发现新版本: " + release.getTag_name()) + new AlertDialog.Builder(IndexActivity.this).setTitle(getString(R.string.index__new_version, release.getTag_name())) .setView(webView) - .setPositiveButton("前往更新", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.index__go_update, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { Uri uri = Uri.parse(release.getHtml_url()); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); } - }).setNegativeButton("取消", new DialogInterface.OnClickListener() { + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); @@ -162,6 +169,9 @@ public void onUpdateFailed(Throwable t) { } }); } + + // 进入主页的trigger + LauncherApplication.getInstance().triggerAtTime(Trigger.TRIGGER_TIME_HOME_PAGE); } /** @@ -208,6 +218,19 @@ public int compare(Entry o1, Entry o2) { return o1.index - o2.index; } }); + mPanel.setLeftIconClickListener(R.drawable.icon_scan, new View.OnClickListener() { + @Override + public void onClick(View v) { + PermissionUtil.requestPermissions(Collections.singletonList(Manifest.permission.CAMERA), IndexActivity.this, new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(boolean result, String reason) { + Intent intent = new Intent(IndexActivity.this, QRScanActivity.class); + intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_OTHER); + startActivity(intent); + } + }); + } + }); CustomAdapter adapter = new CustomAdapter(this, entries); if (entries.size() <= 3) { @@ -218,18 +241,21 @@ public int compare(Entry o1, Entry o2) { mGridView.setAdapter(adapter); // 有写权限,申请下 - PatchRequest.updatePatchList(); + PatchRequest.updatePatchList(null); } /** * 加载其他信息 */ private void loadOthers() { - // 检查是否需要上报故障日志 BackgroundExecutor.execute(new Runnable() { @Override public void run() { + // 检查是否需要上报故障日志 checkErrorLog(); + + // 读取外部的ADB秘钥 + readOuterAdbKey(); } }); } @@ -333,7 +359,7 @@ public void run() { } }); } else { - toastLong("日志打包失败"); + toastLong(getString(R.string.index__package_crash_failed)); // 回设检查时间,以便下次上报 SPService.putLong(SPService.KEY_ERROR_CHECK_TIME, errorTime - 10); @@ -342,6 +368,26 @@ public void run() { }); } + /** + * 读取外部ADB配置文件 + */ + private void readOuterAdbKey() { + File root = FileUtils.getSubDir("adb"); + final File adbKey = new File(root, "adbkey"); + final File pubKey = new File(root, "adbkey.pub"); + if (!adbKey.exists() || !pubKey.exists()) { + return; + } + + boolean result = CmdTools.readOuterAdbKey(adbKey, pubKey); + if (!result) { + toastShort("拷贝ADB Key失败"); + } else { + adbKey.delete(); + pubKey.delete(); + } + } + public static class Entry { private int iconId; @@ -356,10 +402,43 @@ public static class Entry { private Class targetActivity; public Entry(EntryActivity activity, Class target) { - this.iconId = activity.icon(); + if (activity.icon() != -1) { + this.iconId = activity.icon(); + } else if (!StringUtil.isEmpty(activity.iconName())) { + // 反射获取id + String name = activity.iconName(); + int lastDotPos = name.lastIndexOf('.'); + String clazz = name.substring(0, lastDotPos); + String field = name.substring(lastDotPos + 1); + try { + Class RClass = ClassUtil.getClassByName(clazz); + Field icon = RClass.getDeclaredField(field); + this.iconId = icon.getInt(null); + } catch (Exception e) { + LogUtil.e(TAG, "Fail to load icon result with id:" + name); + this.iconId = R.drawable.solopi_main; + } + } else { + this.iconId = R.drawable.solopi_main; + } String name = activity.name(); - if (activity.nameRes() > 0) { + if (activity.nameRes() != 0) { name = StringUtil.getString(activity.nameRes()); + } else if (StringUtil.isNotEmpty(activity.nameResName())) { + int nameRes = 0; + String nameResName = activity.nameResName(); + int lastDotPos = nameResName.lastIndexOf('.'); + String clazz = nameResName.substring(0, lastDotPos); + String field = nameResName.substring(lastDotPos + 1); + try { + Class RClass = ClassUtil.getClassByName(clazz); + Field nameResF = RClass.getDeclaredField(field); + nameRes = nameResF.getInt(null); + } catch (Exception e) { + LogUtil.e(TAG, "Fail to load name result with id:" + nameResName); + nameRes = R.string.app_name; + } + name = StringUtil.getString(nameRes); } this.name = name; permissions = activity.permissions(); @@ -468,6 +547,8 @@ public View getView(int position, View convertView, ViewGroup parent) { if (item.saturation != 1F) { viewHolder.background.setSaturation(item.saturation); + } else { + viewHolder.background.setSaturation(1); } convertView.setOnClickListener(new View.OnClickListener() { diff --git a/src/app/src/main/java/com/alipay/hulu/activity/InfoActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/InfoActivity.java index fabd531..4af23c6 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/InfoActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/InfoActivity.java @@ -17,7 +17,7 @@ import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.text.Html; import android.text.method.LinkMovementMethod; import android.view.View; @@ -49,7 +49,7 @@ public void onClick(View v) { InfoActivity.this.finish(); } }); - panel.setMiddleTitle("关于"); + panel.setMiddleTitle(getString(R.string.activity__about)); TextView versionName = (TextView) findViewById(R.id.version_name); versionName.setText(getString(R.string.info__version_text, SystemUtil.getAppVersionName())); diff --git a/src/app/src/main/java/com/alipay/hulu/activity/LicenseActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/LicenseActivity.java index de6054d..7a3fbfd 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/LicenseActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/LicenseActivity.java @@ -16,7 +16,7 @@ package com.alipay.hulu.activity; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.view.View; import android.webkit.WebView; @@ -43,7 +43,7 @@ public void onClick(View v) { finish(); } }); - panel.setMiddleTitle("开源许可"); + panel.setMiddleTitle(getString(R.string.activity__license)); final WebView licenseText = (WebView) findViewById(R.id.license_text); licenseText.loadUrl(NOTICE_HTML); diff --git a/src/app/src/main/java/com/alipay/hulu/activity/LocalReplayResultActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/LocalReplayResultActivity.java new file mode 100644 index 0000000..2a52893 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/activity/LocalReplayResultActivity.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.activity; + +import android.os.Bundle; +import android.view.View; + +import com.alipay.hulu.R; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.utils.MiscUtil; +import com.alipay.hulu.fragment.LocalReplayResultListFragment; +import com.alipay.hulu.ui.HeadControlPanel; +import com.google.android.material.tabs.TabLayout; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; + +public class LocalReplayResultActivity extends BaseActivity { + private HeadControlPanel panel; + private TabLayout tabLayout; + private ViewPager viewPager; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_replay_result); + + initView(); + + initControl(); + } + + private void initView() { + panel = _findViewById(R.id.head_replay_list); + tabLayout = findViewById(R.id.replay_result_tab); + viewPager = findViewById(R.id.replay_result_list_pager); + } + + private void initControl() { + InjectorService.g().register(this); + panel.setMiddleTitle(getString(R.string.activity_local_replay_result_title)); + panel.setBackIconClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + + viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout)); + tabLayout.setupWithViewPager(viewPager); + tabLayout.setTabGravity(TabLayout.GRAVITY_FILL); + tabLayout.setTabMode(TabLayout.MODE_FIXED); + tabLayout.setSelectedTabIndicatorColor(getResources().getColor(R.color.mainBlue)); + tabLayout.post(new Runnable() { + @Override + public void run() { + MiscUtil.setIndicator(tabLayout, 0, 0); + } + }); + + LocalReplayResultPagerAdapter pagerAdapter = new LocalReplayResultPagerAdapter(getSupportFragmentManager()); + viewPager.setAdapter(pagerAdapter); + viewPager.setOffscreenPageLimit(2); + } + + private static class LocalReplayResultPagerAdapter extends FragmentPagerAdapter { + + private static final int[] PAGES = LocalReplayResultListFragment.getAvailableTypes(); + + public LocalReplayResultPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + return LocalReplayResultListFragment.newInstance(PAGES[position]); + } + + @Override + public CharSequence getPageTitle(int position) { + return LocalReplayResultListFragment.getTypeName(PAGES[position]); + } + @Override + public int getCount() { + return PAGES.length; + } + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/activity/MyApplication.java b/src/app/src/main/java/com/alipay/hulu/activity/MyApplication.java index 43b82c0..0094833 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/MyApplication.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/MyApplication.java @@ -19,13 +19,16 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.WindowManager; +import com.alipay.hulu.R; import com.alipay.hulu.bean.CaseStepHolder; import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.common.application.LauncherApplication; @@ -41,8 +44,10 @@ import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.service.CaseReplayManager; +import com.alipay.hulu.service.InstallReceiver; import com.alipay.hulu.shared.io.db.GreenDaoManager; import com.alipay.hulu.util.LargeObjectHolder; +import com.alipay.hulu.util.SystemUtil; import com.liulishuo.filedownloader.FileDownloader; import java.io.File; @@ -60,6 +65,8 @@ import java.util.Timer; import java.util.TimerTask; +import cn.dreamtobe.filedownloader.OkHttp3Connection; + public class MyApplication extends LauncherApplication { private static final String TAG = "MyApplication"; @@ -164,7 +171,7 @@ public void run() { lastTime = appInfo; String app = content[1].split(":")[1].trim(); - // 如果发现了葫芦娃或者目标应用的Anr信息 + // 如果发现了SoloPi或者目标应用的Anr信息 if (StringUtil.equals(getInstance().appPackage, app) || StringUtil.equals(app, MyApplication.getInstance().getPackageName())) { LogUtil.w(TAG, "Find anr info: " + app); @@ -181,7 +188,7 @@ public void run() { LogUtil.w(TAG, "Copy anr file result: " + result); - MyApplication.getInstance().showToast("发现anr信息,已拷贝至: " + pathInShell); + MyApplication.getInstance().showToast(getString(R.string.app__find_anr_info, pathInShell)); } } } @@ -249,7 +256,6 @@ public void onFinish(List resultBeans, Context context) { @Override public void init() { - sInstance = this; // 注册自身信息 @@ -279,6 +285,24 @@ public void run() { @Override protected void initInMain() { super.initInMain(); + // 加载版本信息 + try { + PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0); + SystemUtil.VERSION_NAME = pInfo.versionName; //version name + SystemUtil.VERSION_CODE = pInfo.versionCode; //version code + } catch (PackageManager.NameNotFoundException e) { + LogUtil.e(TAG, "Fail to load my app version info", e); + } + + // Android 8.0及以上,显式监控应用状态 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + IntentFilter intentFilter =new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addDataScheme("package"); + registerReceiver(new InstallReceiver(), intentFilter); + } + registerLifecycleCallbacks(); } @@ -348,6 +372,11 @@ public void onActivityDestroyed(Activity activity) { } }); + + // Init the FileDownloader with the OkHttp3Connection.Creator. + FileDownloader.setupOnApplicationOnCreate(this) + .connectionCreator(new OkHttp3Connection.Creator()) + .commit(); } /** @@ -423,7 +452,7 @@ public void invalidTempAppInfo() { this.appName = appName[0]; } else { this.appPackage = "-"; - this.appName = "全局"; + this.appName = getString(R.string.constant_global); } } injectorService.pushMessage(SubscribeParamEnum.APP, appPackage, true); @@ -471,7 +500,7 @@ public Map getAppAndAppName() { this.appName = appName[0]; } else { this.appPackage = "-"; - this.appName = "全局"; + this.appName = getString(R.string.constant_global); } } @@ -483,7 +512,7 @@ public Map getAppAndAppName() { private void initLibraries() { initGreenDao(); - initFileDownloader(); +// initFileDownloader(); curSysInputMethod = Settings.Secure.getString(getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); diff --git a/src/app/src/main/java/com/alipay/hulu/activity/NewRecordActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/NewRecordActivity.java index 8460c44..09f7504 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/NewRecordActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/NewRecordActivity.java @@ -20,8 +20,8 @@ import android.content.pm.PackageInfo; import android.os.Bundle; import android.provider.Settings; -import android.support.annotation.Nullable; -import android.support.v4.widget.DrawerLayout; +import androidx.annotation.Nullable; +import androidx.drawerlayout.widget.DrawerLayout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -32,7 +32,6 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; import com.alibaba.fastjson.JSON; import com.alipay.hulu.R; @@ -45,7 +44,6 @@ import com.alipay.hulu.common.injector.param.SubscribeParamEnum; import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; -import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.GlideUtil; @@ -56,13 +54,16 @@ import com.alipay.hulu.replay.OperationStepProvider; import com.alipay.hulu.service.CaseRecordManager; import com.alipay.hulu.service.CaseReplayManager; +import com.alipay.hulu.shared.io.OperationStepService; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; import com.alipay.hulu.shared.io.db.GreenDaoManager; +import com.alipay.hulu.shared.io.db.OperationLogHandler; import com.alipay.hulu.shared.io.db.RecordCaseInfoDao; import com.alipay.hulu.shared.node.action.RunningModeEnum; import com.alipay.hulu.shared.node.utils.AppUtil; import com.alipay.hulu.shared.node.utils.PrepareUtil; import com.alipay.hulu.ui.HeadControlPanel; +import com.alipay.hulu.util.CaseReplayUtil; import com.alipay.hulu.util.SystemUtil; import java.util.Arrays; @@ -71,7 +72,7 @@ /** * Created by lezhou.wyl on 2018/2/1. */ -@EntryActivity(icon = R.drawable.icon_luxiang, nameRes = R.string.record__name, permissions = {"adb", "float", "toast:请将Soloπ添加到后台白名单中"}, index = 1, cornerText = "New", cornerPersist = 3, cornerBg = 0xFFFF5900) +@EntryActivity(iconName = "com.alipay.hulu.R$drawable.icon_luxiang", nameResName = "com.alipay.hulu.R$string.activity__record", permissions = {"adb", "float", "background", "toast:${com.alipay.hulu.R$string.toast_message__add_solopi_background}", "powerSave"}, index = 1, cornerText = "New", cornerPersist = 3, cornerBg = 0xFFFF5900) public class NewRecordActivity extends BaseActivity { private static final String TAG = NewRecordActivity.class.getSimpleName(); @@ -150,24 +151,15 @@ protected void onNewIntent(Intent intent) { private void initRecentCaseLayout() { - mRecentCaseListView = (ListView) findViewById(R.id.recent_case_list); + mRecentCaseListView = findViewById(R.id.recent_case_list); mEmptyView = findViewById(R.id.empty_hint); mCheckAllCasesBtn = findViewById(R.id.check_all_cases); mRecentCaseAdapter = new ReplayListAdapter(this); - mRecentCaseAdapter.setOnEditClickListener(new AdapterView.OnItemClickListener() { + mRecentCaseAdapter.setOnPlayClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { RecordCaseInfo caseInfo = (RecordCaseInfo) mRecentCaseAdapter.getItem(position); - if (caseInfo == null) { - return; - } - caseInfo = caseInfo.clone(); - - // 启动编辑页 - Intent intent = new Intent(NewRecordActivity.this, CaseEditActivity.class); - int storeId = CaseStepHolder.storeCase(caseInfo); - intent.putExtra(CaseEditActivity.RECORD_CASE_EXTRA, storeId); - startActivity(intent); + playCase(caseInfo); } }); mRecentCaseListView.setAdapter(mRecentCaseAdapter); @@ -182,61 +174,85 @@ public void onClick(View v) { mRecentCaseListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - final RecordCaseInfo caseInfo = (RecordCaseInfo) mRecentCaseAdapter.getItem(position); - if (caseInfo == null) { - return; - } + RecordCaseInfo caseInfo = (RecordCaseInfo) mRecentCaseAdapter.getItem(position); + editCase(caseInfo); + } + }); - // 检查权限 - PermissionUtil.requestPermissions(Arrays.asList("adb", Settings.ACTION_ACCESSIBILITY_SETTINGS), NewRecordActivity.this, new PermissionUtil.OnPermissionCallback() { - @Override - public void onPermissionResult(boolean result, String reason) { - if (result) { - showProgressDialog(getString(R.string.record__preparing)); + } - BackgroundExecutor.execute(new Runnable() { - @Override - public void run() { - boolean prepareResult = PrepareUtil.doPrepareWork(caseInfo.getTargetAppPackage(), new PrepareUtil.PrepareStatus() { - @Override - public void currentStatus(int progress, int total, String message, boolean status) { - updateProgressDialog(progress, total, message); - } - }); + /** + * 编辑用例 + * @param caseInfo + */ + private void editCase(RecordCaseInfo caseInfo) { + if (caseInfo == null) { + return; + } - if (prepareResult) { - runOnUiThread(new Runnable() { - @Override - public void run() { - dismissProgressDialog(); - startReplay(caseInfo); - startTargetApp(caseInfo.getTargetAppPackage()); - } - }); - } else { - runOnUiThread(new Runnable() { - @Override - public void run() { - dismissProgressDialog(); - Toast.makeText(NewRecordActivity.this, R.string.record__prepare_failed, Toast.LENGTH_SHORT).show(); - } - }); - } + caseInfo = caseInfo.clone(); + + // 启动编辑页 + Intent intent = new Intent(NewRecordActivity.this, CaseEditActivity.class); + int storeId = CaseStepHolder.storeCase(caseInfo); + intent.putExtra(CaseEditActivity.RECORD_CASE_EXTRA, storeId); + startActivity(intent); + } + + /** + * 执行用例 + * @param caseInfo + */ + private void playCase(final RecordCaseInfo caseInfo) { + if (caseInfo == null) { + return; + } +// 检查权限 + PermissionUtil.requestPermissions(Arrays.asList("adb", Settings.ACTION_ACCESSIBILITY_SETTINGS), NewRecordActivity.this, new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(boolean result, String reason) { + if (result) { + showProgressDialog(getString(R.string.record__preparing)); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + boolean prepareResult = PrepareUtil.doPrepareWork(caseInfo.getTargetAppPackage(), new PrepareUtil.PrepareStatus() { + @Override + public void currentStatus(int progress, int total, String message, boolean status) { + updateProgressDialog(progress, total, message); } }); + + if (prepareResult) { + runOnUiThread(new Runnable() { + @Override + public void run() { + dismissProgressDialog(); + CaseReplayUtil.startReplay(caseInfo); +// startTargetApp(caseInfo.getTargetAppPackage()); + } + }); + } else { + runOnUiThread(new Runnable() { + @Override + public void run() { + dismissProgressDialog(); + toastShort(getString(R.string.record__prepare_env_fail)); + } + }); + } } - } - }); + }); + } } }); - } private void initHeadPanel() { mPanel = (HeadControlPanel) findViewById(R.id.head_layout); - mPanel.setMiddleTitle(getString(R.string.record__name)); + mPanel.setMiddleTitle(getString(R.string.activity__record)); mPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -343,7 +359,7 @@ private void initAppHeadView() { @Override public void onClick(View v) { if (StringUtil.isEmpty(mCaseName.getText().toString().trim())) { - Toast.makeText(NewRecordActivity.this, R.string.record__case_name_empty, Toast.LENGTH_SHORT).show(); + toastShort(R.string.record__case_name_empty); return; } @@ -376,7 +392,6 @@ public void onClick(View v) { @Override public void onPermissionResult(boolean result, String reason) { if (result) { - showProgressDialog(getString(R.string.record__preparing)); BackgroundExecutor.execute(new Runnable() { @@ -394,6 +409,9 @@ public void currentStatus(int progress, int total, String message, boolean statu @Override public void run() { dismissProgressDialog(); + + LauncherApplication.service(OperationStepService.class).registerStepProcessor(new OperationLogHandler()); + startRecord(caseInfo); startTargetApp(caseInfo.getTargetAppPackage()); } @@ -403,7 +421,7 @@ public void run() { @Override public void run() { dismissProgressDialog(); - Toast.makeText(NewRecordActivity.this, R.string.record__prepare_failed, Toast.LENGTH_SHORT).show(); + toastShort(R.string.record__prepare_failed); } }); } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/NewReplayListActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/NewReplayListActivity.java index ad24656..a1c4d03 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/NewReplayListActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/NewReplayListActivity.java @@ -16,25 +16,24 @@ package com.alipay.hulu.activity; import android.content.Intent; -import android.graphics.Color; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.alipay.hulu.R; -import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.fragment.ReplayListFragment; import com.alipay.hulu.ui.HeadControlPanel; +import com.google.android.material.tabs.TabLayout; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; /** * Created by lezhou.wyl on 2018/7/30. @@ -45,7 +44,6 @@ public class NewReplayListActivity extends BaseActivity { private ViewPager mPager; private TabLayout mTabLayout; private HeadControlPanel mHeadPanel; - private TextView rightTitle; @Override @@ -54,25 +52,48 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { setContentView(R.layout.activity_new_replay_list); - mPager = (ViewPager) findViewById(R.id.pager); - mTabLayout = (TabLayout) findViewById(R.id.tab_layout); - mHeadPanel = (HeadControlPanel) findViewById(R.id.head_replay_list); + mPager = findViewById(R.id.pager); + mTabLayout = findViewById(R.id.tab_layout); + mHeadPanel = findViewById(R.id.head_replay_list); // 配置菜单信息 - rightTitle = new TextView(this); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.setMarginEnd(ContextUtil.dip2px(this, 16)); - rightTitle.setTextColor(Color.WHITE); - rightTitle.setText(R.string.constant__batch_replay); + LayoutInflater inflater = LayoutInflater.from(this); + + View rightTitle = inflater.inflate(R.layout.item_icon_template, mHeadPanel, false); + ImageView icon = rightTitle.findViewById(R.id.item_icon_template_icon); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + rightTitle.setLayoutParams(params); + TextView title = rightTitle.findViewById(R.id.item_icon_template_title); + title.setText(R.string.constant__batch_replay); + icon.setImageResource(R.drawable.icon_batch_play); + params.setMarginEnd(- getResources().getDimensionPixelSize(R.dimen.control_dp4)); + params.setMarginStart(getResources().getDimensionPixelSize(R.dimen.control_dp8)); + rightTitle.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + NewReplayListActivity.this.startActivity(new Intent(NewReplayListActivity.this, BatchExecutionActivity.class)); + } + }); + mHeadPanel.addMenuFromLeft(rightTitle); + + rightTitle = inflater.inflate(R.layout.item_icon_template, mHeadPanel, false); + icon = rightTitle.findViewById(R.id.item_icon_template_icon); + params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + rightTitle.setLayoutParams(params); + title = rightTitle.findViewById(R.id.item_icon_template_title); + title.setText(R.string.replay_icon__history); + icon.setImageResource(R.drawable.icon_replay_history); + params.setMarginStart(getResources().getDimensionPixelSize(R.dimen.control_dp8)); rightTitle.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - startActivity(new Intent(NewReplayListActivity.this, BatchExecutionActivity.class)); + NewReplayListActivity.this.startActivity(new Intent(NewReplayListActivity.this, LocalReplayResultActivity.class)); } }); mHeadPanel.addMenuFromLeft(rightTitle); - mHeadPanel.setMiddleTitle(getString(R.string.constant__case_list)); + mHeadPanel.setMiddleTitle(getString(R.string.activity__case_list)); + mHeadPanel.setTitlePosition(HeadControlPanel.POSITION_LEFT); mHeadPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -93,6 +114,7 @@ public void run() { ReplayPagerAdapter pagerAdapter = new ReplayPagerAdapter(getSupportFragmentManager()); mPager.setAdapter(pagerAdapter); + mPager.setOffscreenPageLimit(2); } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/PatchStatusActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/PatchStatusActivity.java new file mode 100644 index 0000000..dd0450c --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/activity/PatchStatusActivity.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.activity; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.os.Bundle; +import androidx.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.alipay.hulu.R; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.utils.ClassUtil; +import com.alipay.hulu.common.utils.patch.PatchLoadResult; +import com.alipay.hulu.ui.HeadControlPanel; +import com.alipay.hulu.upgrade.PatchRequest; + +import java.util.ArrayList; +import java.util.List; + +public class PatchStatusActivity extends BaseActivity { + private HeadControlPanel header; + private ListView patchList; + private BaseAdapter patchItemAdapter; + private View emptyView; + + private final List patches = new ArrayList<>(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_patch_status); + + initView(); + initData(); + } + + private void initView() { + header = _findViewById(R.id.patch_status_header); + patchList = _findViewById(R.id.patch_status_list); + emptyView = findViewById(R.id.patch_status_empty_view); + + patchList.setEmptyView(emptyView); + + header.setMiddleTitle(getString(R.string.settings__plugin_list)); + + + header.setBackIconClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + } + + + private void initData() { + final LayoutInflater inflater = LayoutInflater.from(this); + + patchItemAdapter = new BaseAdapter() { + @Override + public int getCount() { + return patches.size(); + } + + @Override + public Object getItem(int position) { + return patches.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, final ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.item_patch_status, parent, false); + View delete = convertView.findViewById(R.id.item_patch_delete); + delete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final PatchLoadResult patch = (PatchLoadResult) v.getTag(); + new AlertDialog.Builder(PatchStatusActivity.this) + .setMessage(getString(R.string.patch_status__delete_plugin, patch.name)) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ClassUtil.removePatch(patch.name); + dialog.dismiss(); + reloadData(); + } + }) + .setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).show(); + } + }); + } + + PatchLoadResult patch = (PatchLoadResult) getItem(position); + + TextView title = (TextView) convertView.findViewById(R.id.item_patch_name); + TextView version = (TextView) convertView.findViewById(R.id.item_patch_version); + TextView filter = (TextView) convertView.findViewById(R.id.item_patch_filter); + View delete = convertView.findViewById(R.id.item_patch_delete); + + title.setText(patch.name); + version.setText(getString(R.string.patch_status__version_code, patch.version)); + filter.setText(patch.filter); + delete.setTag(patch); + + return convertView; + } + }; + patchList.setAdapter(patchItemAdapter); + + header.setInfoIconClickListener(R.drawable.icon_reload, new View.OnClickListener() { + @Override + public void onClick(View v) { + showProgressDialog(getString(R.string.patch_status__loading_plugin)); + PatchRequest.updatePatchList(new PatchRequest.LoadPatchCallback() { + @Override + public void onLoaded() { + dismissProgressDialog(); + toastShort(R.string.patch_status__load_success); + + // 避免过快插件还未加载完毕 + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + reloadData(); + } + }, 1000); + } + + @Override + public void onFailed() { + dismissProgressDialog(); + toastShort(R.string.patch_status__load_failed); + } + }); + } + }); + + reloadData(); + } + + /** + * 通知数据变化 + */ + private void reloadData() { + patches.clear(); + patches.addAll(ClassUtil.getAllPatches()); + patchItemAdapter.notifyDataSetChanged(); + } + +} diff --git a/src/app/src/main/java/com/alipay/hulu/activity/PerformanceActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/PerformanceActivity.java index e881016..26cd535 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/PerformanceActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/PerformanceActivity.java @@ -21,9 +21,8 @@ import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v7.widget.AppCompatSpinner; -import android.util.Pair; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatSpinner; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -43,13 +42,11 @@ import com.alipay.hulu.common.injector.param.SubscribeParamEnum; import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; -import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.ClassUtil; import com.alipay.hulu.common.utils.GlideUtil; import com.alipay.hulu.common.utils.LogUtil; -import com.alipay.hulu.common.utils.PatchProcessUtil; import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.common.utils.patch.PatchLoadResult; @@ -59,13 +56,12 @@ import com.alipay.hulu.shared.node.utils.PrepareUtil; import com.alipay.hulu.ui.HeadControlPanel; -import java.io.File; import java.util.List; /** * Created by lezhou.wyl on 2018/1/28. */ -@EntryActivity(icon = R.drawable.icon_xingneng, name = "性能工具", permissions = {"adb", "float"}, index = 2) +@EntryActivity(iconName = "com.alipay.hulu.R$drawable.icon_xingneng", nameResName = "com.alipay.hulu.R$string.activity__performance_test", permissions = {"adb", "float", "background", "powerSave"}, index = 2) public class PerformanceActivity extends BaseActivity { private String TAG = "PerformanceFragment"; @@ -99,7 +95,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { mPerfStressAdapter = new PerformStressAdapter(this); mPanel = (HeadControlPanel) findViewById(R.id.head_layout); - mPanel.setMiddleTitle("性能测试"); + mPanel.setMiddleTitle(getString(R.string.activity__performance_test)); mPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -214,7 +210,7 @@ public boolean isEmpty() { public void onItemSelected(AdapterView parent, View view, int position, long id) { // 全局特殊处理 if (position == 0) { - ((MyApplication)getApplication()).updateAppAndName("-", "全局"); + ((MyApplication)getApplication()).updateAppAndName("-", getString(com.alipay.hulu.common.R.string.constant__global)); } else { ApplicationInfo info = listPack.get(position - 1); LogUtil.i(TAG, "Select info: " + StringUtil.hide(info.packageName)); @@ -237,15 +233,15 @@ public void onNothingSelected(AdapterView parent) { @Override public void onClick(View v) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - toastShort("此功能不支持Android5.0以下设备"); + toastShort(getString(R.string.performance__not_support_for_android_l)); return; } if (ClassUtil.getPatchInfo(VideoAnalyzer.SCREEN_RECORD_PATCH) == null) { - LauncherApplication.getInstance().showDialog(PerformanceActivity.this, "是否加载录屏耗时计算插件?", "是", new Runnable() { + LauncherApplication.getInstance().showDialog(PerformanceActivity.this, getString(R.string.performance__load_record_plugin), getString(R.string.constant__yes), new Runnable() { @Override public void run() { - showProgressDialog("插件下载中"); + showProgressDialog(getString(R.string.performance__downloading_plugin)); BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -258,7 +254,7 @@ public void currentStatus(int progress, int total, String message, boolean statu if (rs == null) { // 降级到网络模式 dismissProgressDialog(); - toastLong("无法加载计算插件"); + toastLong(getString(R.string.performance__load_plugin_failed)); return; } @@ -268,7 +264,7 @@ public void currentStatus(int progress, int total, String message, boolean statu }); } - }, "否", null); + }, getString(R.string.constant__no), null); return; } @@ -284,8 +280,7 @@ public void onGrantSuccess() { @Override public void onGrantFail(String msg) { - toastLong("设备需要开启ADB 5555端口并授权调试才可使用" + - "\n请在命令行执行 adb tcpip 5555"); + toastLong(getString(R.string.performance__grant_adb)); } }); } @@ -309,4 +304,10 @@ public void onClick(View v) { mStressListView.setHeaderDividersEnabled(false); } + + @Override + protected void onDestroy() { + super.onDestroy(); + mPerfStressAdapter.stop(); + } } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/PerformanceChartActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/PerformanceChartActivity.java index 9bbae63..3ccd671 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/PerformanceChartActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/PerformanceChartActivity.java @@ -15,19 +15,16 @@ */ package com.alipay.hulu.activity; -import android.Manifest; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.ActivityCompat; -import android.support.v7.widget.AppCompatSpinner; import android.view.View; import android.widget.AdapterView; import android.widget.SimpleAdapter; import android.widget.TextView; -import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatSpinner; import com.alipay.hulu.R; import com.alipay.hulu.common.service.SPService; @@ -41,13 +38,18 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileFilter; -import java.io.FileReader; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -70,13 +72,14 @@ public class PerformanceChartActivity extends BaseActivity { private static final String TAG = "PerfChartAct"; private static final FileFilter folderFilter = new FileFilter() { - Pattern newPattern = Pattern.compile("\\d{2}月\\d{2}日\\d{2}:\\d{2}:\\d{2}-\\d{2}月\\d{2}日\\d{2}:\\d{2}:\\d{2}"); + Pattern newPattern = Pattern.compile("\\d{14}_\\d{14}"); + Pattern midPattern = Pattern.compile("\\d{2}月\\d{2}日\\d{2}:\\d{2}:\\d{2}-\\d{2}月\\d{2}日\\d{2}:\\d{2}:\\d{2}"); Pattern oldPattern = Pattern.compile("\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}_\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"); @Override public boolean accept(File file) { // 记录所有文件夹 - return file.isDirectory() && (newPattern.matcher(file.getName()).matches() || oldPattern.matcher(file.getName()).matches()); + return file.isDirectory() && (newPattern.matcher(file.getName()).matches() || midPattern.matcher(file.getName()).matches() || oldPattern.matcher(file.getName()).matches()); } }; @@ -131,7 +134,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { private void initView(){ setContentView(R.layout.activity_record_chart); headPanel = (HeadControlPanel) findViewById(R.id.head_layout); - headPanel.setMiddleTitle("录制数据"); + headPanel.setMiddleTitle(getString(R.string.activity__performance_display)); headPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -227,7 +230,7 @@ public void onItemSelected(AdapterView parent, View view, int position, long } if (realPattern == null) { - Toast.makeText(PerformanceChartActivity.this, "录制数据未找到,请重新打开应用", Toast.LENGTH_SHORT).show(); + toastShort(R.string.performance_chart__no_record_data); return; } @@ -239,8 +242,18 @@ public void onItemSelected(AdapterView parent, View view, int position, long } else { // 重新构造录制文件名称 File f = new File(currentFolder, realPattern.getName() + "_" + realPattern.getSource() + "_" + realPattern.getStartTime() + "_" + realPattern.getEndTime() + ".csv"); + + // 加载编码信息 + String charsetName = SPService.getString(SPService.KEY_OUTPUT_CHARSET, "GBK"); + Charset charset; try { - BufferedReader reader = new BufferedReader(new FileReader(f)); + charset = Charset.forName(charsetName); + } catch (UnsupportedCharsetException e) { + LogUtil.w(TAG, "unsupported charset for name=" + charsetName, e); + charset = Charset.forName("UTF-8"); + } + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(f), charset)); String dataTitle = reader.readLine(); // 首行定义数据单位 if (dataTitle != null) { @@ -254,7 +267,7 @@ public void onItemSelected(AdapterView parent, View view, int position, long while ((line = reader.readLine()) != null) { String[] contents = line.split(","); LogUtil.d(TAG, "read line: %s", Arrays.toString(contents)); - if (contents.length == 3) { + if (contents.length == 3 || contents.length == 4) { RecordPattern.RecordItem item = new RecordPattern.RecordItem(Long.parseLong(contents[0]), Float.parseFloat(contents[1]), contents[2]); records.add(item); } else if (contents.length == 2) { @@ -402,12 +415,21 @@ public int compare(RecordPattern lhs, RecordPattern rhs) { } titles.clear(); - String[] sortedTimeKeys = records.keySet().toArray(new String[records.size()]); - // 时间从小到大排序 - Arrays.sort(sortedTimeKeys); - // 反过来从大到小取时间 - for (int i = sortedTimeKeys.length - 1; i > -1; i--) { - String key = sortedTimeKeys[i]; + // 按修改时间从大到小排序 + Collections.sort(folders, new Comparator() { + @Override + public int compare(File o1, File o2) { + return Long.valueOf(o2.lastModified()).compareTo(o1.lastModified()); + } + }); + + // 按顺序保存 + for (File f: folders) { + String key = f.getName(); + if (!records.containsKey(key)) { + continue; + } + Map item = new HashMap<>(1); item.put("title", key); titles.add(item); @@ -496,6 +518,6 @@ private void calculateSummary(List recordItems) { averange = 0f; } - summaryText.setText(String.format("∫f(x): %.2f 平均值: %.2f 最小值: %.2f 最大值: %.2f", total, averange / count, min, max)); + summaryText.setText(String.format(Locale.CHINA, getString(R.string.performance__summary), total, averange / count, min, max)); } } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/QRScanActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/QRScanActivity.java index 99156bf..8adc272 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/QRScanActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/QRScanActivity.java @@ -16,12 +16,11 @@ package com.alipay.hulu.activity; import android.Manifest; +import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.PointF; +import android.net.Uri; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.design.widget.Snackbar; -import android.support.v4.app.ActivityCompat; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; @@ -31,12 +30,18 @@ import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.injector.provider.Provider; +import com.alipay.hulu.common.scheme.SchemeActivity; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.event.HandlePermissionEvent; import com.alipay.hulu.event.ScanSuccessEvent; -import com.dlazaro66.qrcodereaderview.QRCodeReaderView; -import com.dlazaro66.qrcodereaderview.QRCodeReaderView.OnQRCodeReadListener; +import com.alipay.hulu.shared.scan.ScanCodeType; +import com.alipay.hulu.ui.AnyCodeReaderView; +import com.google.android.material.snackbar.Snackbar; +import com.google.zxing.BarcodeFormat; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; /** @@ -45,7 +50,7 @@ @Provider({@Param(type = ScanSuccessEvent.class, sticky = false), @Param(type = HandlePermissionEvent.class, sticky = false)}) public class QRScanActivity extends BaseActivity - implements ActivityCompat.OnRequestPermissionsResultCallback, OnQRCodeReadListener { + implements ActivityCompat.OnRequestPermissionsResultCallback, AnyCodeReaderView.OnCodeReadListener { private static final String TAG = "QRScanActivity"; public static final String KEY_SCAN_TYPE = "KEY_SCAN_TYPE"; @@ -55,7 +60,8 @@ public class QRScanActivity extends BaseActivity private ViewGroup mainLayout; private TextView resultTextView; - private QRCodeReaderView qrCodeReaderView; + private TextView resultTypeText; + private AnyCodeReaderView anyCodeReaderView; private volatile boolean isQRCodeReadListenerEnabled = false; private InjectorService injectorService; @@ -95,18 +101,18 @@ protected void onResume() { } private void enableQRCodeReadListener() { - if (qrCodeReaderView != null) { + if (anyCodeReaderView != null) { isQRCodeReadListenerEnabled = true; - qrCodeReaderView.startCamera(); - qrCodeReaderView.setOnQRCodeReadListener(this); + anyCodeReaderView.startCamera(); + anyCodeReaderView.setOnCodeReadListener(this); } } private void disableQRCodeReadListener() { - if (qrCodeReaderView != null) { + if (anyCodeReaderView != null) { isQRCodeReadListenerEnabled = false; - qrCodeReaderView.stopCamera(); - qrCodeReaderView.setOnQRCodeReadListener(null); + anyCodeReaderView.stopCamera(); + anyCodeReaderView.setOnCodeReadListener(null); } } @@ -133,29 +139,64 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis } @Override - public void onQRCodeRead(String text, PointF[] points) { + public void onCodeRead(BarcodeFormat format, String text, PointF[] points) { LogUtil.d(TAG, "OnQrCodeRead"); if (!isQRCodeReadListenerEnabled) { return; } resultTextView.setText(text); + resultTypeText.setText(format.toString()); + + // 过滤不可用的类型 + ScanCodeType acceptType = ScanCodeType.getByFormat(format); + if (acceptType == null) { + LogUtil.w(TAG, "Can't process code of type::" + format); + enableQRCodeReadListener(); + return; + } disableQRCodeReadListener(); if (StringUtil.isEmpty(text)) { + enableQRCodeReadListener(); return; } long curTime = System.currentTimeMillis(); if (curTime - lastReadTime < 2000) { + enableQRCodeReadListener(); return; } lastReadTime = curTime; - if (curScanType == ScanSuccessEvent.SCAN_TYPE_SCHEME) { - notifyScanSuccess(text); + if (curScanType == ScanSuccessEvent.SCAN_TYPE_SCHEME + || curScanType == ScanSuccessEvent.SCAN_TYPE_QR_CODE + || curScanType == ScanSuccessEvent.SCAN_TYPE_BAR_CODE) { + notifyScanSuccess(text, acceptType); + } else if (curScanType == ScanSuccessEvent.SCAN_TYPE_PARAM) { + if (StringUtil.startWith(text, "http://") || StringUtil.startWith(text, "https://")) { + notifyScanSuccess(text, acceptType); + } else { + resultTextView.setText(getString(R.string.qr_scan__url_not_support, text)); + enableQRCodeReadListener(); + } + } else { + resultTextView.setText(text); + if (StringUtil.startWith(text, "http")) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(text)); + startActivity(intent); + finish(); + } else if (StringUtil.startWith(text, "solopi://")) { + Intent intent = new Intent(this, SchemeActivity.class); + intent.setData(Uri.parse(text)); + startActivity(intent); + finish(); + } else { + enableQRCodeReadListener(); + } } } @@ -169,12 +210,11 @@ protected void onDestroy() { /** * 发送成功消息 - * - * @param content */ - public void notifyScanSuccess(String content) { + public void notifyScanSuccess(String text, ScanCodeType codeType) { ScanSuccessEvent event = new ScanSuccessEvent(); - event.setContent(content); + event.setContent(text); + event.setCodeType(codeType); event.setType(curScanType); injectorService.pushMessage(null, event); finish(); @@ -203,13 +243,14 @@ public void onClick(View view) { private void initQRCodeReaderView() { View content = getLayoutInflater().inflate(R.layout.content_decoder, mainLayout, true); - qrCodeReaderView = (QRCodeReaderView) content.findViewById(R.id.qrdecoderview); + anyCodeReaderView = content.findViewById(R.id.anydecoderview); resultTextView = (TextView) content.findViewById(R.id.result_text_view); + resultTypeText = content.findViewById(R.id.result_type_text); - qrCodeReaderView.setAutofocusInterval(2000L); - qrCodeReaderView.setOnQRCodeReadListener(this); - qrCodeReaderView.setBackCamera(); - qrCodeReaderView.startCamera(); + anyCodeReaderView.setAutofocusInterval(2000L); + anyCodeReaderView.setOnCodeReadListener(this); + anyCodeReaderView.setBackCamera(); + anyCodeReaderView.startCamera(); enableQRCodeReadListener(); } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/RecordManageActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/RecordManageActivity.java index db3391e..eb5a9b3 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/RecordManageActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/RecordManageActivity.java @@ -15,26 +15,25 @@ */ package com.alipay.hulu.activity; -import android.app.Activity; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; -import android.widget.Toast; import com.alipay.hulu.R; -import com.alipay.hulu.common.service.SPService; +import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.ui.HeadControlPanel; import java.io.File; +import java.io.FileFilter; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.regex.Pattern; @@ -44,6 +43,11 @@ public class RecordManageActivity extends BaseActivity { private static final String TAG = "RecordManageActivity"; + private static Pattern newPattern = Pattern.compile("\\d{14}_\\d{14}"); + private static Pattern midPattern = Pattern.compile("\\d{2}月\\d{2}日\\d{2}:\\d{2}:\\d{2}-\\d{2}月\\d{2}日\\d{2}:\\d{2}:\\d{2}"); + private static Pattern oldPattern = Pattern.compile("\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}_\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"); + + // Views private HeadControlPanel headPanel; @@ -75,7 +79,7 @@ private void initView() { setContentView(R.layout.activity_record_manage); headPanel = (HeadControlPanel) findViewById(R.id.head_layout); - headPanel.setMiddleTitle("性能数据管理"); + headPanel.setMiddleTitle(getString(R.string.activity__performance_manage)); headPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -169,7 +173,7 @@ private void deleteSelectFolders(String[] select) { } if (!folder.delete()) { - Toast.makeText(this, "文件夹\"" + folderName + "\"无法删除,请手动删除", Toast.LENGTH_LONG).show(); + LauncherApplication.toast(R.string.record__fail_delete_folder, folder); } } } @@ -180,20 +184,28 @@ private void deleteSelectFolders(String[] select) { */ private void refreshRecords() { if (recordDir != null && recordDir.exists() && recordDir.isDirectory()) { - File[] files = recordDir.listFiles(); - recordFolderNames.clear(); - LogUtil.i(TAG, "get files " + StringUtil.hide(files)); - - Pattern newPattern = Pattern.compile("\\d{2}月\\d{2}日\\d{2}:\\d{2}:\\d{2}-\\d{2}月\\d{2}日\\d{2}:\\d{2}:\\d{2}"); - Pattern oldPattern = Pattern.compile("\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}_\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"); - - // 记录所有文件夹 - for (File file : files) { - if (file.isDirectory() && (newPattern.matcher(file.getName()).matches() || oldPattern.matcher(file.getName()).matches())) { - recordFolderNames.add(file.getName()); + // 记录所有相关文件夹 + File[] list = recordDir.listFiles(new FileFilter() { + @Override + public boolean accept(File file) { + return file.isDirectory() && (newPattern.matcher(file.getName()).matches() || midPattern.matcher(file.getName()).matches() || oldPattern.matcher(file.getName()).matches()); } + }); + LogUtil.i(TAG, "get files " + StringUtil.hide(list)); + + // 修改顺序排序 + Arrays.sort(list, new Comparator() { + @Override + public int compare(File o1, File o2) { + return Long.valueOf(o2.lastModified()).compareTo(o1.lastModified()); + } + }); + + recordFolderNames.clear(); + for (File f: list) { + recordFolderNames.add(f.getName()); } - Collections.sort(recordFolderNames); + LogUtil.i(TAG, "get folders: " + recordFolderNames.size()); } } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/SettingsActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/SettingsActivity.java index 092b9c4..b9342f9 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/SettingsActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/SettingsActivity.java @@ -18,21 +18,18 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; -import android.support.v7.app.AlertDialog; +import androidx.appcompat.app.AlertDialog; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.ScrollView; import android.widget.TextView; -import android.widget.Toast; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONException; +import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.R; -import com.alipay.hulu.common.constant.Constant; +import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.AESUtils; @@ -53,6 +50,11 @@ import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; import com.alipay.hulu.ui.HeadControlPanel; import com.alipay.hulu.upgrade.PatchRequest; +import com.alipay.hulu.util.DialogUtils; +import com.alipay.hulu.util.DialogUtils.OnDialogResultListener; +import com.zhy.view.flowlayout.FlowLayout; +import com.zhy.view.flowlayout.TagAdapter; +import com.zhy.view.flowlayout.TagFlowLayout; import java.io.BufferedReader; import java.io.File; @@ -65,6 +67,8 @@ import java.util.List; import java.util.Map; +import static com.alipay.hulu.util.DialogUtils.showMultipleEditDialog; + /** * Created by lezhou.wyl on 01/01/2018. */ @@ -85,15 +89,47 @@ public class SettingsActivity extends BaseActivity { private View mPatchListWrapper; private TextView mPatchListInfo; + private View mReplayOtherAppSettingWrapper; + private TextView mReplayOtherAppInfo; + + private View mRestartAppSettingWrapper; + private TextView mRestartAppInfo; + + private View mGlobalParamSettingWrapper; + private View mResolutionSettingWrapper; private TextView mResolutionSettingInfo; private View mHightlightSettingWrapper; private TextView mHightlightSettingInfo; + private View mLanguageSettingWrapper; + private TextView mLanguageSettingInfo; + private View mDisplaySystemAppSettingWrapper; private TextView mDisplaySystemAppSettingInfo; + private View mAutoReplaySettingWrapper; + private TextView mAutoReplaySettingInfo; + + private View mRecordCoverModeSettingWrapper; + private TextView mRecordCoverModeSettingInfo; + + private View mSkipAccessibilitySettingWrapper; + private TextView mSkipAccessibilitySettingInfo; + + private View mMaxWaitSettingWrapper; + private TextView mMaxWaitSettingInfo; + + private View mMaxScrollFindSettingWrapper; + private TextView mMaxScrollFindSettingInfo; + + private View mDefaultRotationSettingWrapper; + private TextView mDefaultRotationSettingInfo; + + private View mChangeRotationSettingWrapper; + private TextView mChangeRotationSettingInfo; + private View mCheckUpdateSettingWrapper; private TextView mCheckUpdateSettingInfo; @@ -112,6 +148,9 @@ public class SettingsActivity extends BaseActivity { private View mHideLogSettingWrapper; private TextView mHideLogSettingInfo; + private View mAdbServerSettingWrapper; + private TextView mAdbServerSettingInfo; + private View mImportCaseSettingWrapper; private View mImportPluginSettingWrapper; @@ -142,10 +181,68 @@ public void onClick(View v) { } }); + mGlobalParamSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showGlobalParamEdit(); + } + }); + + mDefaultRotationSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setItems(R.array.default_screen_rotation, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String item = getResources().getStringArray(R.array.default_screen_rotation)[which]; + SPService.putInt(SPService.KEY_SCREEN_FACTOR_ROTATION, which); + if (which == 1 || which == 3) { + SPService.putBoolean(SPService.KEY_SCREEN_ROTATION, true); + mChangeRotationSettingInfo.setText(R.string.constant__yes); + } else { + SPService.putBoolean(SPService.KEY_SCREEN_ROTATION, false); + mChangeRotationSettingInfo.setText(R.string.constant__no); + } + mDefaultRotationSettingInfo.setText(item); + } + }) + .setTitle(R.string.setting__set_screen_orientation) + .setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .show(); + } + }); + + mChangeRotationSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.setting__change_screen_axis) + .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_SCREEN_ROTATION, true); + mChangeRotationSettingInfo.setText(R.string.constant__yes); + } + }).setNegativeButton(R.string.constant__no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_SCREEN_ROTATION, false); + mChangeRotationSettingInfo.setText(R.string.constant__no); + } + }).show(); + } + }); + mRecordUploadWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showMultipleEditDialog(new OnDialogResultListener() { + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { @Override public void onDialogPositive(List data) { if (data.size() == 1) { @@ -165,7 +262,7 @@ public void onDialogPositive(List data) { mRecordScreenUploadWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showMultipleEditDialog(new OnDialogResultListener() { + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { @Override public void onDialogPositive(List data) { if (data.size() == 1) { @@ -185,7 +282,7 @@ public void onDialogPositive(List data) { mPatchListWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showMultipleEditDialog(new OnDialogResultListener() { + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { @Override public void onDialogPositive(List data) { if (data.size() == 1) { @@ -197,7 +294,7 @@ public void onDialogPositive(List data) { mPatchListInfo.setText(path); // 更新patch列表 - PatchRequest.updatePatchList(); + PatchRequest.updatePatchList(null); } } } @@ -210,7 +307,7 @@ public void onDialogPositive(List data) { mOutputCharsetSettingWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showMultipleEditDialog(new OnDialogResultListener() { + showMultipleEditDialog(SettingsActivity.this, new DialogUtils.OnDialogResultListener() { @Override public void onDialogPositive(List data) { if (data.size() == 1) { @@ -226,6 +323,82 @@ public void onDialogPositive(List data) { }); + mLanguageSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setTitle(R.string.settings__language) + .setItems(R.array.language, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putInt(SPService.KEY_USE_LANGUAGE, which); + LauncherApplication.getInstance().setApplicationLanguage(); + + mLanguageSettingInfo.setText(getResources().getStringArray(R.array.language)[which]); + // 重启服务 + LauncherApplication.getInstance().restartAllServices(); + + Intent intent = new Intent(SettingsActivity.this, SplashActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + } + }) + .setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).show(); + } + }); + + mReplayOtherAppSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.settings__should_replay_in_other_app) + .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_ALLOW_REPLAY_DIFFERENT_APP, true); + mReplayOtherAppInfo.setText(R.string.constant__yes); + dialog.dismiss(); + } + }).setNegativeButton(R.string.constant__no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_ALLOW_REPLAY_DIFFERENT_APP, false); + mReplayOtherAppInfo.setText(R.string.constant__no); + dialog.dismiss(); + } + }).show(); + } + }); + + mRestartAppSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.settings__should_restart_before_replay) + .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_RESTART_APP_ON_PLAY, true); + mRestartAppInfo.setText(R.string.constant__yes); + dialog.dismiss(); + } + }).setNegativeButton(R.string.constant__no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_RESTART_APP_ON_PLAY, false); + mRestartAppInfo.setText(R.string.constant__no); + dialog.dismiss(); + } + }).show(); + } + }); + mDisplaySystemAppSettingWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -251,11 +424,118 @@ public void onClick(DialogInterface dialog, int which) { } }); + mAutoReplaySettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.setting__auto_replay) + .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_REPLAY_AUTO_START, true); + mAutoReplaySettingInfo.setText(R.string.constant__yes); + dialog.dismiss(); + } + }).setNegativeButton(R.string.constant__no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_REPLAY_AUTO_START, false); + mAutoReplaySettingInfo.setText(R.string.constant__no); + dialog.dismiss(); + } + }).show(); + } + }); + + mRecordCoverModeSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.setting__choose_action_block_mode) + .setPositiveButton(R.string.setting__conver_mode, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_RECORD_COVER_MODE, true); + mRecordCoverModeSettingInfo.setText(R.string.setting__conver_mode); + dialog.dismiss(); + } + }).setNegativeButton(R.string.setting__block_mode, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_RECORD_COVER_MODE, false); + mRecordCoverModeSettingInfo.setText(R.string.setting__block_mode); + dialog.dismiss(); + } + }).show(); + } + }); + + mSkipAccessibilitySettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.setting__skip_accessibility) + .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_SKIP_ACCESSIBILITY, true); + mSkipAccessibilitySettingInfo.setText(R.string.constant__yes); + dialog.dismiss(); + } + }).setNegativeButton(R.string.constant__no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_SKIP_ACCESSIBILITY, false); + mSkipAccessibilitySettingInfo.setText(R.string.constant__no); + dialog.dismiss(); + } + }).show(); + } + }); + + mMaxWaitSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { + @Override + public void onDialogPositive(List data) { + if (data.size() == 1) { + String time = data.get(0); + SPService.putLong(SPService.KEY_MAX_WAIT_TIME, Long.parseLong(time)); + mMaxWaitSettingInfo.setText(time + "ms"); + } + } + }, getString(R.string.settings__max_wait_time), Collections.singletonList(new Pair<>(getString(R.string.setting__max_wait_time), Long.toString(SPService.getLong(SPService.KEY_MAX_WAIT_TIME, 10000))))); + } + }); + + + mMaxScrollFindSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List> data = new ArrayList<>(2); + data.add(new Pair<>(getString(R.string.settings__max_scroll_find_count), "" + SPService.getLong(SPService.KEY_MAX_SCROLL_FIND_COUNT, 0))); + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { + @Override + public void onDialogPositive(List data) { + if (data.size() != 1) { + LogUtil.e("SettingActivity", "获取编辑项不为1项"); + return; + } + + // 更新截图分辨率信息 + SPService.putInt(SPService.KEY_MAX_SCROLL_FIND_COUNT, Integer.parseInt(data.get(0))); + mMaxWaitSettingInfo.setText(data.get(0)); + } + }, getString(R.string.settings__max_scroll_find_count), data); + } + }); + mBaseDirSettingWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { FileChooseDialogActivity.startFileChooser(SettingsActivity.this, - REQUEST_FILE_CHOOSE, StringUtil.getString(R.string.settings__base_dir), "solopi", + REQUEST_FILE_CHOOSE, getString(R.string.settings__base_dir), "solopi", FileUtils.getSolopiDir()); } }); @@ -263,7 +543,7 @@ public void onClick(View v) { mAesSeedSettingWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showMultipleEditDialog(new OnDialogResultListener() { + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { @Override public void onDialogPositive(List data) { if (data.size() == 1) { @@ -285,7 +565,7 @@ public void onDialogPositive(List data) { mClearFilesSettingWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showMultipleEditDialog(new OnDialogResultListener() { + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { @Override public void onDialogPositive(List data) { if (data.size() == 1) { @@ -300,7 +580,7 @@ public void onDialogPositive(List data) { SPService.putInt(SPService.KEY_AUTO_CLEAR_FILES_DAYS, daysNum); mClearFilesSettingInfo.setText(days); } else { - Toast.makeText(SettingsActivity.this, R.string.settings__config_failed, Toast.LENGTH_SHORT).show(); + toastShort(R.string.settings__config_failed); } } } @@ -312,8 +592,8 @@ public void onDialogPositive(List data) { @Override public void onClick(View v) { List> data = new ArrayList<>(2); - data.add(new Pair<>("图像查找截图分辨率", "" + SPService.getInt(SPService.KEY_SCREENSHOT_RESOLUTION, 720))); - showMultipleEditDialog(new OnDialogResultListener() { + data.add(new Pair<>(getString(R.string.settings__screenshot_resolution), "" + SPService.getInt(SPService.KEY_SCREENSHOT_RESOLUTION, 720))); + showMultipleEditDialog(SettingsActivity.this, new OnDialogResultListener() { @Override public void onDialogPositive(List data) { if (data.size() != 2) { @@ -325,7 +605,7 @@ public void onDialogPositive(List data) { SPService.putInt(SPService.KEY_SCREENSHOT_RESOLUTION, Integer.parseInt(data.get(0))); mResolutionSettingInfo.setText(data.get(0) + "P"); } - }, "图像查找截图设置", data); + }, getString(R.string.settings__screenshot_setting), data); } }); @@ -333,7 +613,7 @@ public void onDialogPositive(List data) { @Override public void onClick(View v) { new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) - .setMessage("回放时是否高亮待操作控件?") + .setMessage(R.string.settings__highlight_node) .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -376,6 +656,25 @@ public void onClick(DialogInterface dialog, int which) { } }); + // adb调试地址 + mAdbServerSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showMultipleEditDialog(SettingsActivity.this, new DialogUtils.OnDialogResultListener() { + @Override + public void onDialogPositive(List data) { + if (data.size() == 1) { + String server = data.get(0); + SPService.putString(SPService.KEY_ADB_SERVER, server); + mAdbServerSettingInfo.setText(server); + } + } + }, getString(R.string.settings__adb_server), + Collections.singletonList(new Pair<>(getString(R.string.settings__adb_server), + SPService.getString(SPService.KEY_ADB_SERVER, "localhost:5555")))); + } + }); + mHideLogSettingWrapper.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -503,74 +802,9 @@ public boolean accept(File dir, String name) { }); } - private interface OnDialogResultListener { - void onDialogPositive(List data); - } - - /** - * 为多个字段配置输入框 - * - * @param title - * @param data - */ - private void showMultipleEditDialog(final OnDialogResultListener listener, String title, List> data) { - ScrollView v = (ScrollView) LayoutInflater.from(ContextUtil.getContextThemeWrapper( - SettingsActivity.this, R.style.AppDialogTheme)) - .inflate(R.layout.dialog_setting, null); - LinearLayout view = (LinearLayout) v.getChildAt(0); - final List editTexts = new ArrayList<>(); - - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - // 对每一个字段添加EditText - for (Pair source : data) { - EditText edit = new EditText(this); - - // 配置字段 - edit.setHint(source.first); - edit.setText(source.second); - - // 设置其他参数 - edit.setTextColor(getResources().getColor(R.color.primaryText)); - edit.setHintTextColor(getResources().getColor(R.color.secondaryText)); - edit.setTextSize(18); - edit.setHighlightColor(getResources().getColor(R.color.colorAccent)); - - view.addView(edit, layoutParams); - editTexts.add(edit); - } - - // 显示Dialog - new AlertDialog.Builder(SettingsActivity.this, R.style.AppDialogTheme) - .setTitle(title) - .setView(v) - .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - List result = new ArrayList<>(editTexts.size() + 1); - - // 获取每个编辑框的文字 - for (EditText data : editTexts) { - result.add(data.getText().toString().trim()); - } - - if (listener != null) { - listener.onDialogPositive(result); - } - dialog.dismiss(); - } - }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - } - }).setCancelable(true) - .show(); - } - private void initView() { mPanel = (HeadControlPanel) findViewById(R.id.head_layout); - mPanel.setMiddleTitle(getString(R.string.constant__setting)); + mPanel.setMiddleTitle(getString(R.string.activity__setting)); mRecordScreenUploadWrapper = findViewById(R.id.recordscreen_upload_setting_wrapper); mRecordScreenUploadInfo = (TextView) findViewById(R.id.recordscreen_upload_setting_info); @@ -595,10 +829,23 @@ private void initView() { path = SPService.getString(SPService.KEY_PATCH_URL, "https://raw.githubusercontent.com/alipay/SoloPi/master/.json"); if (StringUtil.isEmpty(path)) { - mPatchListInfo.setText("未设置"); + mPatchListInfo.setText(R.string.settings__unset); } else { mPatchListInfo.setText(path); } + mGlobalParamSettingWrapper = findViewById(R.id.global_param_setting_wrapper); + + mDefaultRotationSettingWrapper = findViewById(R.id.default_screen_rotation_setting_wrapper); + mDefaultRotationSettingInfo = _findViewById(R.id.default_screen_rotation_setting_info); + int defaultRotation = SPService.getInt(SPService.KEY_SCREEN_FACTOR_ROTATION, 0); + String[] arrays = getResources().getStringArray(R.array.default_screen_rotation); + mDefaultRotationSettingInfo.setText(arrays[defaultRotation]); + + mChangeRotationSettingWrapper = findViewById(R.id.change_rotation_setting_wrapper); + mChangeRotationSettingInfo = _findViewById(R.id.change_rotation_setting_info); + boolean changeRotation = SPService.getBoolean(SPService.KEY_SCREEN_ROTATION, false); + mChangeRotationSettingInfo.setText(changeRotation? R.string.constant__yes: R.string.constant__no); + mOutputCharsetSettingWrapper = findViewById(R.id.output_charset_setting_wrapper); mOutputCharsetSettingInfo = (TextView) findViewById(R.id.output_charset_setting_info); @@ -612,6 +859,25 @@ private void initView() { mHightlightSettingInfo = (TextView) findViewById(R.id.replay_highlight_setting_info); mHightlightSettingInfo.setText(SPService.getBoolean(SPService.KEY_HIGHLIGHT_REPLAY_NODE, true)? R.string.constant__yes: R.string.constant__no); + mRecordCoverModeSettingWrapper = findViewById(R.id.record_cover_mode_setting_wrapper); + mRecordCoverModeSettingInfo = _findViewById(R.id.record_cover_mode_setting_info); + boolean coverMode = SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false); + if (coverMode) { + mRecordCoverModeSettingInfo.setText(R.string.setting__conver_mode); + } else { + mRecordCoverModeSettingInfo.setText(R.string.setting__block_mode); + } + + mLanguageSettingWrapper = findViewById(R.id.language_setting_wrapper); + mLanguageSettingInfo = (TextView) findViewById(R.id.language_setting_info); + int pos = SPService.getInt(SPService.KEY_USE_LANGUAGE, 0); + String[] availableLanguages = getResources().getStringArray(R.array.language); + if (availableLanguages != null && availableLanguages.length > pos) { + mLanguageSettingInfo.setText(availableLanguages[pos]); + } else { + mLanguageSettingInfo.setText(availableLanguages[0]); + } + mDisplaySystemAppSettingWrapper = findViewById(R.id.display_system_app_setting_wrapper); mDisplaySystemAppSettingInfo = (TextView) findViewById(R.id.display_system_app_setting_info); boolean displaySystemApp = SPService.getBoolean(SPService.KEY_DISPLAY_SYSTEM_APP, false); @@ -621,6 +887,49 @@ private void initView() { mDisplaySystemAppSettingInfo.setText(R.string.constant__no); } + mAutoReplaySettingWrapper = findViewById(R.id.auto_replay_setting_wrapper); + mAutoReplaySettingInfo = (TextView) findViewById(R.id.auto_replay_setting_info); + boolean autoReplay = SPService.getBoolean(SPService.KEY_REPLAY_AUTO_START, false); + if (autoReplay) { + mAutoReplaySettingInfo.setText(R.string.constant__yes); + } else { + mAutoReplaySettingInfo.setText(R.string.constant__no); + } + + mReplayOtherAppSettingWrapper = findViewById(R.id.replay_other_app_setting_wrapper); + mReplayOtherAppInfo = _findViewById(R.id.replay_other_app_setting_info); + boolean replayOtherApp = SPService.getBoolean(SPService.KEY_ALLOW_REPLAY_DIFFERENT_APP, false); + mReplayOtherAppInfo.setText(replayOtherApp? R.string.constant__yes: R.string.constant__no); + + mRestartAppSettingWrapper = findViewById(R.id.restart_app_setting_wrapper); + mRestartAppInfo = _findViewById(R.id.restart_app_setting_info); + boolean restartApp = SPService.getBoolean(SPService.KEY_RESTART_APP_ON_PLAY, true); + mRestartAppInfo.setText(restartApp? R.string.constant__yes: R.string.constant__no); + + mAdbServerSettingWrapper = findViewById(R.id.adb_server_setting_wrapper); + mAdbServerSettingInfo = _findViewById(R.id.adb_server_setting_info); + mAdbServerSettingInfo.setText(SPService.getString(SPService.KEY_ADB_SERVER, "localhost:5555")); + + mSkipAccessibilitySettingWrapper = findViewById(R.id.skip_accessibility_setting_wrapper); + mSkipAccessibilitySettingInfo = (TextView) findViewById(R.id.skip_accessibility_setting_info); + boolean skipAccessibility = SPService.getBoolean(SPService.KEY_SKIP_ACCESSIBILITY, true); + if (skipAccessibility) { + mSkipAccessibilitySettingInfo.setText(R.string.constant__yes); + } else { + mSkipAccessibilitySettingInfo.setText(R.string.constant__no); + } + + mMaxWaitSettingWrapper = findViewById(R.id.max_wait_setting_wrapper); + mMaxWaitSettingInfo = (TextView) findViewById(R.id.max_wait_setting_info); + long maxWaitTime = SPService.getLong(SPService.KEY_MAX_WAIT_TIME, 10000L); + mMaxWaitSettingInfo.setText(maxWaitTime + "ms"); + + + mMaxScrollFindSettingWrapper = findViewById(R.id.max_scroll_find_setting_wrapper); + mMaxScrollFindSettingInfo = _findViewById(R.id.max_scroll_find_setting_info); + mMaxScrollFindSettingInfo.setText(Integer.toString(SPService.getInt(SPService.KEY_MAX_SCROLL_FIND_COUNT, 2))); + + mCheckUpdateSettingWrapper = findViewById(R.id.check_update_setting_wrapper); mCheckUpdateSettingInfo = (TextView) findViewById(R.id.check_update_setting_info); boolean checkUpdate = SPService.getBoolean(SPService.KEY_CHECK_UPDATE, true); @@ -630,6 +939,11 @@ private void initView() { mCheckUpdateSettingInfo.setText(R.string.constant__no); } + // 如果不应该展示检测更新部分 + if (!SPService.getBoolean(SPService.KEY_SHOULD_UPDATE_IN_APP, true)) { + mCheckUpdateSettingWrapper.setVisibility(View.GONE); + } + mBaseDirSettingWrapper = findViewById(R.id.base_dir_setting_wrapper); mBaseDirSettingInfo = (TextView) findViewById(R.id.base_dir_setting_info); mBaseDirSettingInfo.setText(FileUtils.getSolopiDir().getPath()); @@ -660,12 +974,116 @@ private void initView() { TextView importPluginPath = (TextView) findViewById(R.id.import_patch_setting_path); importPluginPath.setText(FileUtils.getSubDir("patch").getAbsolutePath()); + + findViewById(R.id.plugin_list_setting_wrapper).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(SettingsActivity.this, PatchStatusActivity.class)); + } + }); + int clearDays = SPService.getInt(SPService.KEY_AUTO_CLEAR_FILES_DAYS, 3); mClearFilesSettingInfo.setText(StringUtil.toString(clearDays)); mAboutBtn = findViewById(R.id.about_wrapper); } + + /** + * 展示全局变量配置窗口 + */ + private void showGlobalParamEdit() { + final List> paramList = new ArrayList<>(); + + String globalParam = SPService.getString(SPService.KEY_GLOBAL_SETTINGS); + JSONObject params = JSON.parseObject(globalParam); + if (params != null && params.size() > 0) { + for (String key: params.keySet()) { + paramList.add(new Pair<>(key, params.getString(key))); + } + } + + final LayoutInflater inflater = LayoutInflater.from(ContextUtil.getContextThemeWrapper( + SettingsActivity.this, R.style.AppDialogTheme)); + final View view = inflater.inflate(R.layout.dialog_global_param_setting, null); + final TagFlowLayout tagFlowLayout = (TagFlowLayout) view.findViewById(R.id.global_param_group); + final EditText paramName= (EditText) view.findViewById(R.id.global_param_name); + final EditText paramValue = (EditText) view.findViewById(R.id.global_param_value); + View paramAdd = view.findViewById(R.id.global_param_add); + + tagFlowLayout.setAdapter(new TagAdapter>(paramList) { + @Override + public View getView(FlowLayout parent, int position, Pair o) { + View root = inflater.inflate(R.layout.item_param_info, parent, false); + + TextView title = (TextView) root.findViewById(R.id.batch_execute_tag_name); + title.setText(getString(R.string.settings__global_param_key_value, o.first, o.second)); + return root; + } + }); + tagFlowLayout.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() { + @Override + public boolean onTagClick(View view, int position, FlowLayout parent) { + paramList.remove(position); + tagFlowLayout.getAdapter().notifyDataChanged(); + return true; + } + }); + + paramAdd.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String key = paramName.getText().toString().trim(); + String value = paramValue.getText().toString().trim(); + if (StringUtil.isEmpty(key) || key.contains("=")) { + toastShort(getString(R.string.setting__invalid_param_name)); + } + + // 清空输入框 + paramName.setText(""); + paramValue.setText(""); + + int replacePosition = -1; + for (int i = 0; i < paramList.size(); i++) { + if (key.equals(paramList.get(i).first)) { + replacePosition = i; + break; + } + } + + // 如果有相同的,就进行替换 + if (replacePosition > -1) { + paramList.set(replacePosition, new Pair<>(key, value)); + } else { + paramList.add(new Pair<>(key, value)); + } + + tagFlowLayout.getAdapter().notifyDataChanged(); + } + }); + + new AlertDialog.Builder(SettingsActivity.this, R.style.AppDialogTheme) + .setView(view) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + JSONObject newGlobalParam = new JSONObject(paramList.size() + 1); + for (Pair param: paramList) { + newGlobalParam.put(param.first, param.second); + } + SPService.putString(SPService.KEY_GLOBAL_SETTINGS, newGlobalParam.toJSONString()); + dialog.dismiss(); + } + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).setCancelable(true) + .show(); + } + + @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_FILE_CHOOSE) { diff --git a/src/app/src/main/java/com/alipay/hulu/activity/SplashActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/SplashActivity.java index 810be39..1e2dd20 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/SplashActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/SplashActivity.java @@ -19,6 +19,7 @@ import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.Handler; @@ -53,7 +54,7 @@ public void onCreate(Bundle savedInstanceState){ // 免责弹窗 boolean showDisplay = SPService.getBoolean(DISPLAY_ALERT_INFO, true); if (showDisplay) { - new AlertDialog.Builder(this).setTitle(R.string.index__disclaimer) + AlertDialog dialog = new AlertDialog.Builder(this).setTitle(R.string.index__disclaimer) .setMessage(R.string.disclaimer) .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { @Override @@ -68,7 +69,9 @@ public void onClick(DialogInterface dialog, int which) { processPemission(); dialog.dismiss(); } - }).show(); + }).setCancelable(false).create(); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); } else { processPemission(); } @@ -77,14 +80,17 @@ public void onClick(DialogInterface dialog, int which) { /** * 写权限后续步骤 */ - private void afterWritePermission() { + private void afterWritePermission(boolean noStart) { FileUtils.getSolopiDir(); Intent intent = new Intent(SplashActivity.this, IndexActivity.class); + if (noStart) { + intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + } // 已经初始化完毕过了,直接进入主页 if (LauncherApplication.getInstance().hasFinishInit()) { startActivity(intent); - finish(); + laterFinish(); } else { // 新启动进闪屏页2s waitForAppInitialize(); @@ -108,7 +114,7 @@ public void run() { Intent intent = new Intent(SplashActivity.this, IndexActivity.class); startActivity(intent); - SplashActivity.this.finish(); + laterFinish(); } }); } @@ -119,7 +125,12 @@ private void processPemission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 如果不存储在/sdcard/solopi,说明已经降级到外置私有目录下了 if (!StringUtil.equals(SPService.getString(SPService.KEY_SOLOPI_PATH_NAME, "solopi"), "solopi")) { - afterWritePermission(); + afterWritePermission(true); + return; + } + + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + afterWritePermission(true); return; } @@ -128,7 +139,7 @@ private void processPemission() { @Override public void onPermissionResult(boolean result, String reason) { if (result) { - afterWritePermission(); + afterWritePermission(false); } else { // 再申请一次 PermissionUtil.requestPermissions(Arrays.asList(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), SplashActivity.this, new PermissionUtil.OnPermissionCallback() { @@ -139,14 +150,14 @@ public void onPermissionResult(boolean result, String reason) { FileUtils.fallBackToExternalDir(SplashActivity.this); } - afterWritePermission(); + afterWritePermission(false); } }); } } }); } else { - afterWritePermission(); + afterWritePermission(true); } } @@ -166,8 +177,20 @@ public void run() { Intent intent = new Intent(SplashActivity.this, IndexActivity.class); startActivity(intent); - SplashActivity.this.finish(); + laterFinish(); } }, 1000); } + + /** + * 稍后结束 + */ + private void laterFinish() { + handler.postDelayed(new Runnable() { + @Override + public void run() { + SplashActivity.this.finish(); + } + }, 500); + } } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/entry/EntryActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/entry/EntryActivity.java index f24db47..87b5ed0 100644 --- a/src/app/src/main/java/com/alipay/hulu/activity/entry/EntryActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/activity/entry/EntryActivity.java @@ -15,6 +15,8 @@ */ package com.alipay.hulu.activity.entry; +import androidx.annotation.StringRes; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -30,7 +32,13 @@ * 图标 * @return */ - int icon(); + int icon() default -1; + + /** + * 图标ID名称 + * @return + */ + String iconName() default ""; /** * 显示名称 @@ -43,8 +51,15 @@ * name string res * @return */ + @StringRes int nameRes() default 0; + /** + * name string res + * @return + */ + String nameResName() default ""; + /** * 依赖权限 * diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/BatchExecutionListAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/BatchExecutionListAdapter.java index ece5a23..98db9f3 100644 --- a/src/app/src/main/java/com/alipay/hulu/adapter/BatchExecutionListAdapter.java +++ b/src/app/src/main/java/com/alipay/hulu/adapter/BatchExecutionListAdapter.java @@ -99,7 +99,7 @@ public View getView(int position, View convertView, ViewGroup parent) { holder.createTime.setText(DateFormat.getDateTimeInstance().format(sDate)); String caseDesc = recordCaseInfo.getCaseDesc(); if (StringUtil.isEmpty(caseDesc)) { - holder.caseDesc.setText("暂无描述"); + holder.caseDesc.setText(R.string.batch_adapter__no_desc); } else { holder.caseDesc.setText(recordCaseInfo.getCaseDesc()); } diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepAdapter.java index 1378007..0937767 100644 --- a/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepAdapter.java +++ b/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepAdapter.java @@ -21,6 +21,8 @@ import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; @@ -34,7 +36,7 @@ import com.alipay.hulu.shared.node.action.PerformActionEnum; import com.alipay.hulu.shared.node.action.provider.ActionProviderManager; import com.alipay.hulu.shared.node.tree.OperationNode; -import com.alipay.hulu.shared.node.tree.export.OperationStepProvider; +import com.alipay.hulu.shared.node.tree.export.OperationStepExporter; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; import com.alipay.hulu.shared.node.utils.BitmapUtil; import com.alipay.hulu.shared.node.utils.LogicUtil; @@ -44,37 +46,56 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.Set; /** * Created by qiaoruikai on 2019/2/18 9:50 PM. */ -public class CaseStepAdapter extends BaseAdapter implements View.OnClickListener, SlideAndDragListView.OnDragDropListener { +public class CaseStepAdapter extends BaseAdapter implements View.OnClickListener, + SlideAndDragListView.OnDragDropListener, CompoundButton.OnCheckedChangeListener { private Context context; private int SCOPE_OFFSET_DP = 10; private List data; + private boolean selectMode = false; + + private Set selectSet; private List runningScope = new ArrayList<>(); private MyDataWrapper currentDragEntity; + private OnStepListener listener; + public CaseStepAdapter(Context context, List data) { this.context = context; this.data = data; + selectSet = new HashSet<>(); reloadScope(); } + public void setListener(OnStepListener listener) { + this.listener = listener; + } + @Override public int getItemViewType(int position) { OperationMethod method = data.get(position).currentStep.getOperationMethod(); // 逻辑操作项不支持继续添加 if (method.getActionEnum() == PerformActionEnum.IF - || method.getActionEnum() == PerformActionEnum.WHILE - || method.getActionEnum() == PerformActionEnum.BREAK - || method.getActionEnum() == PerformActionEnum.CONTINUE) { + || method.getActionEnum() == PerformActionEnum.WHILE) { return 1; + } else if (method.getActionEnum() == PerformActionEnum.BREAK + || method.getActionEnum() == PerformActionEnum.CONTINUE) { + return 2; + } else if (method.getActionEnum() == PerformActionEnum.CLICK) { + return 3; + } else if (method.getActionEnum() == PerformActionEnum.CLICK_IF_EXISTS) { + return 4; } return 0; } @@ -85,9 +106,67 @@ public void notifyDataSetChanged() { super.notifyDataSetChanged(); } + /** + * 设置当前模式 + * @param selectMode + */ + public void setCurrentMode(boolean selectMode) { + this.selectMode = selectMode; + notifyDataSetChanged(); + } + + /** + * 获取选中的IDX + * @return + */ + public List getAndClearSelectOperationSteps() { + reloadScope(); + Set selected = new HashSet<>(selectSet); + selectSet.clear(); + List operations = new ArrayList<>(selected.size() + 1); + if (selected.size() > 0) { + for (MyDataWrapper wrapper : data) { + if (selected.contains(wrapper.idx)) { + operations.add(wrapper); + } + } + } + + notifyDataSetChanged(); + return operations; + } + + /** + * 替换steps为step + * @param idxs + * @param step + */ + public void changeStepsToStep(Set idxs, MyDataWrapper step) { + int position = 0; + boolean findFlag = false; + Iterator wrapperIterator = data.iterator(); + while (wrapperIterator.hasNext()) { + MyDataWrapper wrapper = wrapperIterator.next(); + if (idxs.contains(wrapper.idx)) { + findFlag = true; + wrapperIterator.remove(); + } else if (!findFlag) { + position++; + } + } + + if (findFlag) { + data.add(position, step); + } else { + data.add(step); + } + + notifyDataSetChanged(); + } + @Override public int getViewTypeCount() { - return 2; + return 5; } @Override @@ -102,7 +181,7 @@ public Object getItem(int position) { @Override public long getItemId(int position) { - return 0; + return position; } @Override @@ -124,14 +203,40 @@ public View getView(int position, View convertView, ViewGroup parent) { TextView title = (TextView) convertView.findViewById(R.id.case_step_edit_content_title); TextView param = (TextView) convertView.findViewById(R.id.case_step_edit_content_param); - ImageView icon = (ImageView) convertView.findViewById(R.id.case_step_edit_content_close); - icon.setTag(position); + View movement = convertView.findViewById(R.id.case_step_edit_content_movement); + CheckBox select = (CheckBox) convertView.findViewById(R.id.case_step_edit_content_check); + select.setTag(position); + + ImageView moveTop = (ImageView) movement.findViewById(R.id.case_step_edit_content_move_top); + ImageView moveBottom = (ImageView) movement.findViewById(R.id.case_step_edit_content_move_bottom); + ImageView insert = (ImageView) convertView.findViewById(R.id.case_step_edit_content_insert); - // 如果是第一次加载,设置下ClickListener if (init) { - icon.setOnClickListener(this); + moveTop.setOnClickListener(this); + moveBottom.setOnClickListener(this); + select.setOnCheckedChangeListener(this); + insert.setOnClickListener(this); } + if (selectMode) { + select.setVisibility(View.VISIBLE); + movement.setVisibility(View.GONE); + + if (selectSet.contains(data.get(position).idx)) { + select.setChecked(true); + } else { + select.setChecked(false); + } + } else { + select.setVisibility(View.GONE); + movement.setVisibility(View.VISIBLE); + } + + moveTop.setTag(position); + moveBottom.setTag(position); + insert.setTag(position); + + // 如果是第一次加载,设置下ClickListener List occurred = new ArrayList<>(); int start = -1; List end = new ArrayList<>(); @@ -175,8 +280,8 @@ public View getView(int position, View convertView, ViewGroup parent) { String base64 = null; if (method.containsParam(ImageCompareActionProvider.KEY_TARGET_IMAGE)) { base64 = method.getParam(ImageCompareActionProvider.KEY_TARGET_IMAGE); - } else if (node != null && node.containsExtra(OperationStepProvider.CAPTURE_IMAGE_BASE64)) { - base64 = node.getExtraValue(OperationStepProvider.CAPTURE_IMAGE_BASE64); + } else if (node != null && node.containsExtra(OperationStepExporter.CAPTURE_IMAGE_BASE64)) { + base64 = node.getExtraValue(OperationStepExporter.CAPTURE_IMAGE_BASE64); } // 如果有截图的话,使用截图作为图标 @@ -269,10 +374,46 @@ private String loadTitle(PerformActionEnum actionEnum, OperationMethod method) { } @Override - public void onClick(View v) { + public void onClick(final View v) { + int id = v.getId(); int position = (int) v.getTag(); - data.remove(position); - notifyDataSetChanged(); + if (id == R.id.case_step_edit_content_move_top) { + if (position == 0) { + return; + } + MyDataWrapper wrapper = data.remove(position); + data.add(position - 1, wrapper); + notifyDataSetChanged(); + if (listener != null) { + listener.scroll(-v.getContext().getResources().getDimensionPixelSize(R.dimen.dp_72)); + } + } else if (id == R.id.case_step_edit_content_move_bottom){ + if (position == data.size() - 1) { + return; + } + MyDataWrapper wrapper = data.remove(position); + data.add(position + 1, wrapper); + notifyDataSetChanged(); + if (listener != null) { + listener.scroll(v.getContext().getResources().getDimensionPixelSize(R.dimen.dp_72)); + } + } else if (id == R.id.case_step_edit_content_insert) { + if (listener != null) { + listener.insertAfter(position); + } + } + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + int position = (int) buttonView.getTag(); + MyDataWrapper dataWrapper = data.get(position); + if (isChecked) { + selectSet.add(dataWrapper.idx); + } else { + selectSet.remove(dataWrapper.idx); + } + } public static class MyDataWrapper { @@ -325,6 +466,11 @@ public void onDragDropViewMoved(int fromPosition, int toPosition) { public void onDragViewDown(int finalPosition) { data.set(finalPosition, currentDragEntity); } + + public interface OnStepListener { + void insertAfter(int position); + void scroll(int px); + } } diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepMethodAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepMethodAdapter.java index 27f33b7..7a602a1 100644 --- a/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepMethodAdapter.java +++ b/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepMethodAdapter.java @@ -15,25 +15,38 @@ */ package com.alipay.hulu.adapter; -import android.support.design.widget.TextInputLayout; -import android.support.v7.widget.RecyclerView; +import android.graphics.Bitmap; import android.text.Editable; import android.text.TextWatcher; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; +import android.widget.ImageView; import android.widget.Spinner; import android.widget.TextView; import com.alipay.hulu.R; +import com.alipay.hulu.actions.ImageCompareActionProvider; +import com.alipay.hulu.bean.CaseParamBean; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.node.action.OperationExecutor; import com.alipay.hulu.shared.node.action.OperationMethod; +import com.alipay.hulu.shared.node.utils.BitmapUtil; +import com.alipay.hulu.util.DialogUtils; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; + +import androidx.recyclerview.widget.RecyclerView; import static com.alipay.hulu.shared.node.utils.LogicUtil.SCOPE; @@ -41,23 +54,45 @@ * Created by qiaoruikai on 2019/2/21 9:17 PM. */ public class CaseStepMethodAdapter extends RecyclerView.Adapter { + private static final String TAG = "CaseMethodAdapter"; private List laterList; private OperationMethod method; + private Map paramKeyMap; + List keys; public CaseStepMethodAdapter(List laterList, OperationMethod method) { this.method = method; + + // 解析实际文案 + Map paramMap = method.getActionEnum().getActionParams(); + paramKeyMap = new HashMap<>(); + for (String key: paramMap.keySet()) { + Integer res = paramMap.get(key); + if (res != null) { + paramKeyMap.put(key, StringUtil.getString(res)); + } + } + this.laterList = laterList; // 组装下参数 keys = new ArrayList<>(method.getParamKeys()); + + // 局部操作坐标字段不展示 + keys.remove(OperationExecutor.LOCAL_CLICK_POS_KEY); } @Override public int getItemViewType(int position) { - return StringUtil.equals(keys.get(position), SCOPE)? 1: 0; + String key = keys.get(position); + if (StringUtil.equals(ImageCompareActionProvider.KEY_TARGET_IMAGE, key)) { + return 2; + } + + return StringUtil.equals(key, SCOPE) ? 1 : 0; } @Override @@ -69,6 +104,9 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType if (viewType == 1) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_case_step_edit_select, parent, false); return new SelectAdapter(v, method); + } else if (viewType == 2) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_case_step_edit_image_picker, parent, false); + return new ImageParamHolder(view, method); } else { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_case_step_edit_input, parent, false); return new CaseStepParamHolder(view, method); @@ -78,9 +116,14 @@ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { if (holder instanceof CaseStepParamHolder) { + String key = keys.get(position); + String desc = paramKeyMap.containsKey(key)? paramKeyMap.get(key): key; + String value = method.getParam(key); + ((CaseStepParamHolder) holder).bindData(key, desc, value); + } else if (holder instanceof ImageParamHolder) { String key = keys.get(position); String value = method.getParam(key); - ((CaseStepParamHolder) holder).bindData(key, value); + ((ImageParamHolder) holder).wrapData(key, value); } else { ((SelectAdapter) holder).wrapData(laterList, method.getParam(keys.get(position))); } @@ -145,7 +188,7 @@ public void run() { public void wrapData(List list, String value) { String[] result = new String[list.size()]; int idx = 0; - for (CaseStepAdapter.MyDataWrapper item: list) { + for (CaseStepAdapter.MyDataWrapper item : list) { result[idx++] = item.currentStep.getOperationMethod().getActionEnum().getDesc(); } @@ -163,9 +206,10 @@ public void wrapData(List list, String value) { /** * 用例参数Holder */ - public static class CaseStepParamHolder extends RecyclerView.ViewHolder implements TextWatcher { - private TextInputLayout layout; + public static class CaseStepParamHolder extends RecyclerView.ViewHolder implements TextWatcher, View.OnClickListener { + private TextView title; private EditText editText; + private TextView createParamText; private String key; private String value; @@ -175,17 +219,22 @@ public static class CaseStepParamHolder extends RecyclerView.ViewHolder implemen super(itemView); this.method = method; - layout = (TextInputLayout) itemView.findViewById(R.id.item_case_step_edit_input_layout); - editText = (EditText) itemView.findViewById(R.id.item_case_step_edit_input_edit); + title = (TextView) itemView.findViewById(R.id.item_case_step_name); + editText = (EditText) itemView.findViewById(R.id.item_case_step_edit); editText.addTextChangedListener(this); + + createParamText = (TextView) itemView.findViewById(R.id.item_case_step_create_param); + createParamText.setText(R.string.method_param__set_param); + createParamText.setOnClickListener(this); } - void bindData(String key, String value) { + void bindData(String key, String desc, String value) { this.key = key; this.value = value; editText.setText(value); - layout.setHint(key); + title.setText(desc); + createParamText.setTag(key); } @Override @@ -203,5 +252,50 @@ public void afterTextChanged(Editable s) { value = s.toString(); method.putParam(key, value); } + + @Override + public void onClick(View v) { + final String key = (String) v.getTag(); + DialogUtils.showMultipleEditDialog(v.getContext(), new DialogUtils.OnDialogResultListener() { + @Override + public void onDialogPositive(List data) { + if (data == null || data.size() != 3) { + LogUtil.w(TAG, ""); + return; + } + if (StringUtil.isEmpty(data.get(0))) { + LogUtil.w(TAG, "Param name is empty" + data); + } + + CaseParamBean paramBean = new CaseParamBean(); + paramBean.setParamName(data.get(0)); + paramBean.setParamDesc(data.get(1)); + paramBean.setParamDefaultValue(data.get(2)); + InjectorService.g().pushMessage(null, paramBean); + + value = "${" + data.get(0) + "}"; + method.putParam(key, value); + editText.setText(value); + } + }, "配置参数", Arrays.asList(new Pair<>("参数名", ""), new Pair<>("参数描述", ""), + new Pair<>("默认值", value))); + } + } + + + + public static class ImageParamHolder extends RecyclerView.ViewHolder { + private ImageView imageView; + + public ImageParamHolder(View itemView, OperationMethod method) { + super(itemView); + + imageView = itemView.findViewById(R.id.case_step_edit_image_view); + } + + void wrapData(final String key, String value) { + Bitmap img = BitmapUtil.base64ToBitmap(value); + imageView.setImageBitmap(img); + } } } diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepNodeAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepNodeAdapter.java index 4497c26..c5eed1e 100644 --- a/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepNodeAdapter.java +++ b/src/app/src/main/java/com/alipay/hulu/adapter/CaseStepNodeAdapter.java @@ -15,27 +15,32 @@ */ package com.alipay.hulu.adapter; +import android.graphics.Bitmap; import android.graphics.Rect; -import android.support.annotation.NonNull; -import android.support.design.widget.TextInputLayout; -import android.support.v7.widget.RecyclerView; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; import com.alibaba.fastjson.JSON; import com.alipay.hulu.R; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.node.tree.OperationNode; +import com.alipay.hulu.shared.node.tree.export.OperationStepExporter; +import com.alipay.hulu.shared.node.utils.BitmapUtil; import java.util.ArrayList; import java.util.List; import java.util.Map; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + /** * Created by qiaoruikai on 2019/2/20 8:05 PM. */ @@ -49,15 +54,24 @@ public CaseStepNodeAdapter(@NonNull OperationNode node) { properties = loadPropertiesKey(node); } + @Override + public int getItemViewType(int position) { + return StringUtil.equals(properties.get(position), OperationStepExporter.CAPTURE_IMAGE_BASE64)? 1: 0; + } + @Override public NodePropertyHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (parent == null) { return null; } - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_case_step_edit_input, parent, false); - - return new NodePropertyHolder(view, node); + if (viewType == 0) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_case_step_edit_input, parent, false); + return new TextPropertyHolder(view, node); + } else { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_case_step_edit_image_picker, parent, false); + return new ImagePropertyHolder(view, node); + } } @Override @@ -72,27 +86,38 @@ public int getItemCount() { return properties.size(); } - public static class NodePropertyHolder extends RecyclerView.ViewHolder implements TextWatcher { + static abstract class NodePropertyHolder extends RecyclerView.ViewHolder { + NodePropertyHolder(View itemView) { + super(itemView); + } + + abstract void wrapData(String key, String value); + } + + public static class TextPropertyHolder extends NodePropertyHolder implements TextWatcher { private String key; - private TextInputLayout layout; + private TextView title; private EditText editText; + private TextView infoText; private OperationNode node; - public NodePropertyHolder(View itemView, OperationNode node) { + public TextPropertyHolder(View itemView, OperationNode node) { super(itemView); this.node = node; - layout = (TextInputLayout) itemView.findViewById(R.id.item_case_step_edit_input_layout); - editText = (EditText) itemView.findViewById(R.id.item_case_step_edit_input_edit); + title = (TextView) itemView.findViewById(R.id.item_case_step_name); + editText = (EditText) itemView.findViewById(R.id.item_case_step_edit); editText.addTextChangedListener(this); + infoText = (TextView) itemView.findViewById(R.id.item_case_step_create_param); + infoText.setText(""); } - private void wrapData(String key, String value) { + void wrapData(String key, String value) { this.key = key; editText.setText(value); - layout.setHint(key); + title.setText(key); } @Override @@ -109,13 +134,28 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { public void afterTextChanged(Editable s) { boolean result = updateNodeProperty(key, s.toString(), node); if (!result) { - layout.setError("格式不合法"); + infoText.setText(R.string.node__invalid_param); } else { - layout.setError(null); + infoText.setText(""); } } } + public static class ImagePropertyHolder extends NodePropertyHolder { + private ImageView imageView; + + public ImagePropertyHolder(View itemView, OperationNode node) { + super(itemView); + imageView = itemView.findViewById(R.id.case_step_edit_image_view); + } + + @Override + void wrapData(final String key, String value) { + Bitmap img = BitmapUtil.base64ToBitmap(value); + imageView.setImageBitmap(img); + } + } + /** * 解析node属性 * @param node @@ -173,6 +213,12 @@ static List loadPropertiesKey(OperationNode node) { list.addAll(extras.keySet()); } + // 截图放最前面 + if (list.contains(OperationStepExporter.CAPTURE_IMAGE_BASE64)) { + list.remove(OperationStepExporter.CAPTURE_IMAGE_BASE64); + list.add(0, OperationStepExporter.CAPTURE_IMAGE_BASE64); + } + return list; } @@ -212,7 +258,7 @@ static boolean updateNodeProperty(String key, String value, OperationNode node) case "nodeBound": // 逗号分隔 String[] split = StringUtil.split(value, ","); - if (split.length != 4) { + if (split == null || split.length != 4) { return false; } try { diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/FloatStressAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/FloatStressAdapter.java new file mode 100644 index 0000000..3d466f3 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/adapter/FloatStressAdapter.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.adapter; + +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.view.View; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.alipay.hulu.R; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.shared.display.items.MemoryTools; +import com.alipay.hulu.tools.PerformStressImpl; + +import java.util.ArrayList; +import java.util.List; + +public class FloatStressAdapter extends SoloBaseRecyclerAdapter { + public FloatStressAdapter(Context context) { + super(context, R.layout.float_stress_item); + init(); + } + + private int cpuCount = 0; + private int cpuPercent = 0; + private int memory = 0; + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_CPU_COUNT)) + public void receiveCpuCount(int count) { + if (count == cpuCount) { + return; + } + + // CPU变了 + cpuCount = count; + List items = getAllData(); + items.get(1).count = cpuCount; + notifyDataSetChanged(); + } + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_CPU_PERCENT)) + public void receiveCpuPercent(int percent) { + if (cpuPercent == percent) { + return; + } + + // CPU占比变了 + cpuPercent = percent; + List items = getAllData(); + items.get(0).count = cpuPercent; + notifyDataSetChanged(); + } + + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_MEMORY)) + public void receiveMemory(int memory) { + if (memory == this.memory) { + return; + } + + // 内存变了 + this.memory = memory; + List items = getAllData(); + StressItem item = items.get(2); + item.count = memory; + item.max = MemoryTools.getTotalMemory(context).intValue(); + notifyDataSetChanged(); + } + + @Override + public SimpleViewHolder generateViewHolder(View view) { + return new FloatViewHolder(view); + } + + private void init() { + InjectorService.g().register(this); + + List stressItemList = new ArrayList<>(); + StressItem map = new StressItem(); + map.type = 0; + map.title = "CPU负载"; + map.unit = "%"; + map.max = 100; + map.count = cpuPercent; + stressItemList.add(map); + + map = new StressItem(); + map.type = 1; + map.title = "CPU占用核数"; + map.unit = "核"; + map.max = Runtime.getRuntime().availableProcessors(); + map.count = cpuCount; + stressItemList.add(map); + + map = new StressItem(); + map.type = 2; + map.title = "内存占用"; + map.unit = "MB"; + map.max = MemoryTools.getTotalMemory(context).intValue(); + map.count = memory; + stressItemList.add(map); + updateDate(stressItemList); + } + + public static class StressItem { + int type; + String title; + String unit; + int max; + int count; + } + + private static class FloatViewHolder extends SimpleViewHolder { + private TextView title; + private TextView unit; + private TextView count; + private SeekBar seekBar; + public FloatViewHolder(@NonNull View itemView) { + super(itemView); + } + + @Override + public void bindView(View base) { + this.title = base.findViewById(R.id.display_stress_title); + this.unit = base.findViewById(R.id.display_stress_data_unit); + this.count = base.findViewById(R.id.display_stress_data); + this.seekBar = base.findViewById(R.id.display_stress_sb); + } + + @Override + public void bindData(StressItem data, int index) { + if (data == null) { + return; + } + title.setText(data.title); + unit.setText(data.unit); + seekBar.setMax(data.max); + if (Build.VERSION.SDK_INT >= 26) { + seekBar.setMin(0); + } + + int type = data.type; + seekBar.setTag(type); + + int countValue = data.count; + if (type == 2) { + if (countValue > data.max / 2) { + count.setTextColor(Color.RED); + count.setText("⚠️" + countValue); + } else { + count.setTextColor(count.getResources().getColor(R.color.secondaryText)); + count.setText("" + countValue); + } + } else { + count.setText("" + countValue); + } + seekBar.setProgress(countValue); + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + int type = (int) seekBar.getTag(); + if (type == 2) { + if (progress > seekBar.getMax() / 2) { + count.setTextColor(Color.RED); + count.setText("⚠️" + progress); + } else { + count.setTextColor(count.getResources().getColor(R.color.secondaryText)); + count.setText("" + progress); + } + } else { + count.setText("" + progress); + } + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + int type = (int) seekBar.getTag(); + String event; + if (type == 0) { + event = PerformStressImpl.PERFORMANCE_STRESS_CPU_PERCENT; + } else if (type == 1) { + event = PerformStressImpl.PERFORMANCE_STRESS_CPU_COUNT; + } else { + event = PerformStressImpl.PERFORMANCE_STRESS_MEMORY; + } + InjectorService.g().pushMessage(event, seekBar.getProgress()); + } + }); + } + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/FloatWinAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/FloatWinAdapter.java index f0d4d50..44623f2 100644 --- a/src/app/src/main/java/com/alipay/hulu/adapter/FloatWinAdapter.java +++ b/src/app/src/main/java/com/alipay/hulu/adapter/FloatWinAdapter.java @@ -16,7 +16,7 @@ package com.alipay.hulu.adapter; import android.content.Context; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/LocalTaskResultListAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/LocalTaskResultListAdapter.java new file mode 100644 index 0000000..acd40db --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/adapter/LocalTaskResultListAdapter.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.adapter; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.alipay.hulu.R; +import com.alipay.hulu.bean.ReplayResultBean; +import com.alipay.hulu.common.bean.DeviceInfo; +import com.alipay.hulu.common.utils.StringUtil; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Locale; + +public class LocalTaskResultListAdapter extends SoloBaseAdapter { + private static DateFormat SIMPLE_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.CHINA); + private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA); + + public LocalTaskResultListAdapter(Context context) { + super(context); + } + + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + if (convertView == null) { + convertView = mInflater.inflate(R.layout.item_replay_result_info, parent, false); + holder = new ViewHolder(); + holder.title = convertView.findViewById(R.id.item_replay_result_title); + holder.deviceInfo = convertView.findViewById(R.id.item_replay_result_device_info); + holder.status = convertView.findViewById(R.id.item_replay_result_status); + holder.runTime = convertView.findViewById(R.id.item_replay_result_run_time); + holder.targetApp = convertView.findViewById(R.id.item_replay_result_target_app); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + ReplayResultBean result = getItem(position); + String title = result.getCaseName(); + holder.title.setText(title); + + DeviceInfo deviceInfo = result.getDeviceInfo(); + if (deviceInfo != null) { + String device = String.format("%s %s - %s", deviceInfo.getBrand(), deviceInfo.getProduct(), deviceInfo.getSystemVersion()); + holder.deviceInfo.setVisibility(View.VISIBLE); + holder.deviceInfo.setText(device); + } else { + holder.deviceInfo.setVisibility(View.GONE); + } + + if (result.getExceptionMessage() != null) { + holder.status.setTextColor(0xfff76262); + holder.status.setText(R.string.constant__fail); + } else { + holder.status.setTextColor(0xff65c0ba); + holder.status.setText(R.string.constant__success); + } + holder.runTime.setText(SIMPLE_FORMAT.format(result.getStartTime()) + " - " + SIMPLE_FORMAT.format(result.getEndTime()) + " [" + DATE_FORMAT.format(result.getStartTime()) + "]"); + + String appName = result.getTargetApp(); + if (StringUtil.isNotEmpty(appName)) { + String appVersion = result.getTargetAppVersion(); + if (StringUtil.isNotEmpty(appVersion)) { + holder.targetApp.setText(mContext.getString(R.string.result_item__app_name_version, appName, appVersion)); + } else { + holder.targetApp.setText(appName); + } + } else if (StringUtil.isNotEmpty(result.getTargetAppPkg())) { + holder.targetApp.setText(result.getTargetAppPkg()); + } + + return convertView; + } + + private static class ViewHolder { + TextView title; + TextView deviceInfo; + TextView status; + TextView runTime; + TextView targetApp; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/ParamListAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/ParamListAdapter.java new file mode 100644 index 0000000..0622ca4 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/adapter/ParamListAdapter.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.adapter; + +import android.content.Context; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.alipay.hulu.R; +import com.alipay.hulu.bean.CaseParamBean; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.util.DialogUtils; + +import java.util.Arrays; +import java.util.List; + +public class ParamListAdapter extends SoloBaseAdapter implements View.OnClickListener { + private static final String TAG = "ParamListAdapter"; + + private Context context; + + public ParamListAdapter(Context context) { + super(context); + this.context = context; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(context).inflate(R.layout.item_case_param, parent, false); + convertView.setOnClickListener(this); + } + + convertView.setTag(position); + + TextView title = convertView.findViewById(R.id.param_item_title); + TextView desc = convertView.findViewById(R.id.param_item_desc); + TextView defaultValue = convertView.findViewById(R.id.param_item_default_value); + + CaseParamBean param = getItem(position); + title.setText(param.getParamName()); + desc.setText(param.getParamDesc()); + defaultValue.setText(param.getParamDefaultValue()); + + return convertView; + } + + @Override + public void onClick(View v) { + int position = (int) v.getTag(); + final CaseParamBean param = getItem(position); + + DialogUtils.showMultipleEditDialog(context, new DialogUtils.OnDialogResultListener() { + @Override + public void onDialogPositive(List data) { + if (data == null || data.size() != 2) { + LogUtil.w(TAG, "Edit param %s failed, not suitable result %s", param, data); + return; + } + + param.setParamDesc(data.get(0)); + param.setParamDefaultValue(data.get(1)); + + notifyDataSetChanged(); + } + }, "编辑参数-" + param.getParamName(), + Arrays.asList(new Pair<>("参数描述", param.getParamDesc()), + new Pair<>("默认值", param.getParamDefaultValue()))); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/PerformStressAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/PerformStressAdapter.java index 179a316..8868147 100644 --- a/src/app/src/main/java/com/alipay/hulu/adapter/PerformStressAdapter.java +++ b/src/app/src/main/java/com/alipay/hulu/adapter/PerformStressAdapter.java @@ -16,6 +16,7 @@ package com.alipay.hulu.adapter; import android.content.Context; +import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -26,6 +27,10 @@ import android.widget.Toast; import com.alipay.hulu.R; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.shared.display.items.MemoryTools; import com.alipay.hulu.tools.PerformStressImpl; @@ -39,13 +44,57 @@ public class PerformStressAdapter extends BaseAdapter { protected static final String TAG = "PerformStressAdapter"; private LayoutInflater mInflater; private List> mData; - private Map isSelected; Context cx; + private int cpuCount = 1; + private int cpuPercent = 0; + private int memory = 0; + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_CPU_COUNT)) + public void receiveCpuCount(int count) { + if (count == cpuCount) { + return; + } + + // CPU变了 + cpuCount = count; + notifyDataSetChanged(); + } + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_CPU_PERCENT)) + public void receiveCpuPercent(int percent) { + if (cpuPercent == percent) { + return; + } + + // CPU占比变了 + cpuPercent = percent; + notifyDataSetChanged(); + } + + + @Subscriber(@Param(PerformStressImpl.PERFORMANCE_STRESS_MEMORY)) + public void receiveMemory(int memory) { + if (memory == this.memory) { + return; + } + + // 内存变了 + this.memory = memory; + notifyDataSetChanged(); + } + + public PerformStressAdapter(Context context) { this.cx = context; mInflater = LayoutInflater.from(context); init(); + InjectorService.g().register(this); + LauncherApplication.service(PerformStressImpl.class); + } + + public void stop() { + InjectorService.g().unregister(this); } // 初始化 @@ -55,23 +104,20 @@ private void init() { Map map = new HashMap(); map.put("img", android.R.drawable.ic_menu_crop); - map.put("title", "CPU负载(%)"); - map.put("process", 0); + map.put("title", cx.getString(R.string.stress__cpu_load)); map.put("max", 100); mData.add(map); map = new HashMap(); map.put("img", android.R.drawable.ic_menu_crop); - map.put("title", "CPU多核(n)"); - map.put("process", 1); + map.put("title", cx.getString(R.string.stress__cpu_core)); map.put("max", getCpuCoreNum()); mData.add(map); map = new HashMap(); map.put("img", android.R.drawable.ic_menu_crop); - map.put("title", "内存占用(m)"); - map.put("process", 0); - map.put("max", MemoryTools.getAvailMemory(cx).intValue()); + map.put("title", cx.getString(R.string.stress__memory)); + map.put("max", MemoryTools.getTotalMemory(cx).intValue()); mData.add(map); } @@ -118,7 +164,27 @@ public View getView(final int position, View convertView, ViewGroup parent) { @Override public void onProgressChanged(SeekBar arg0, int progress, boolean fromUser) { if (fromUser) { - mData.get(position).put("process", progress); + switch (position) { + case 0: + cpuPercent = progress; + break; + case 1: + cpuCount = progress; + break; + case 2: + memory = progress; + break; + default: + return; + } + // 超过一半,将颜色变成红色,提示危险 + if (position == 2) { + if (progress > arg0.getMax() / 2) { + finalHolder.data.setTextColor(Color.RED); + } else { + finalHolder.data.setTextColor(finalHolder.data.getResources().getColor(R.color.secondaryText)); + } + } finalHolder.data.setText(String.valueOf(progress)); } } @@ -133,30 +199,17 @@ public void onStopTrackingTouch(SeekBar seekBar) { LogUtil.i(TAG, "progress:" + (Integer) mData.get(position).get("process") + ";max:" + (Integer) mData.get(position).get("max")); - - PerformStressImpl performStressImpl = PerformStressImpl.getInstanceImpl(); // TODO 改成接口定义通用加压方法 switch (position) { case 0:// CPU占用率 - performStressImpl.performCpuStressByCount((int) mData.get(0).get("process"), (int) mData.get(1) - .get("process")); - // CPUTools.performStress((int) - // mData.get(position).get("process")); - break; + InjectorService.g().pushMessage(PerformStressImpl.PERFORMANCE_STRESS_CPU_PERCENT, cpuPercent); + break; case 1:// CPU多核 - performStressImpl.performCpuStressByCount((int) mData.get(0).get("process"), (int) mData.get(1) - .get("process")); + InjectorService.g().pushMessage(PerformStressImpl.PERFORMANCE_STRESS_CPU_COUNT, cpuCount); break; case 2:// 内存占用 - try { - int allocMemory = MemoryTools.dummyMem((int) mData.get(2).get("process")); - if (allocMemory != seekBar.getProgress()) { - seekBar.setProgress(allocMemory); - } - } catch (OutOfMemoryError e) { - Toast.makeText(cx, "内存不足:" + e,Toast.LENGTH_SHORT).show(); - } - break; + InjectorService.g().pushMessage(PerformStressImpl.PERFORMANCE_STRESS_MEMORY, memory); + break; default: break; } @@ -164,8 +217,26 @@ public void onStopTrackingTouch(SeekBar seekBar) { }); holder.sBar.setMax((Integer) mData.get(position).get("max")); - holder.sBar.setProgress((Integer) mData.get(position).get("process")); - holder.data.setText("0"); + + switch (position) { + case 0: + holder.sBar.setProgress(cpuPercent); + holder.data.setText(Integer.toString(cpuPercent)); + break; + case 1: + holder.sBar.setProgress(cpuCount); + holder.data.setText(Integer.toString(cpuCount)); + break; + case 2: + holder.sBar.setProgress(memory); + if (memory > holder.sBar.getMax() / 2) { + holder.data.setTextColor(Color.RED); + } else { + holder.data.setTextColor(holder.data.getResources().getColor(R.color.secondaryText)); + } + holder.data.setText(Integer.toString(memory)); + break; + } return convertView; } diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/ReplayListAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/ReplayListAdapter.java index 007225e..4ab73a6 100644 --- a/src/app/src/main/java/com/alipay/hulu/adapter/ReplayListAdapter.java +++ b/src/app/src/main/java/com/alipay/hulu/adapter/ReplayListAdapter.java @@ -82,8 +82,8 @@ public View getView(int position, View convertView, ViewGroup parent) { holder.caseName = (TextView) convertView.findViewById(R.id.case_name); holder.caseDesc = (TextView) convertView.findViewById(R.id.case_desc); holder.createTime = (TextView) convertView.findViewById(R.id.create_time); - holder.edit = (RelativeLayout) convertView.findViewById(R.id.case_edit); - holder.edit.setOnClickListener(this); + holder.play = (RelativeLayout) convertView.findViewById(R.id.case_play); + holder.play.setOnClickListener(this); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); @@ -96,11 +96,11 @@ public View getView(int position, View convertView, ViewGroup parent) { holder.createTime.setText(DateFormat.getDateTimeInstance().format(sDate)); String caseDesc = recordCaseInfo.getCaseDesc(); if (StringUtil.isEmpty(caseDesc)) { - holder.caseDesc.setText("暂无描述"); + holder.caseDesc.setText(R.string.replay_list__no_desc); } else { holder.caseDesc.setText(recordCaseInfo.getCaseDesc()); } - holder.edit.setTag(position); + holder.play.setTag(position); } return convertView; } @@ -137,7 +137,7 @@ public void deleteCaseById(long id) { } } - public void setOnEditClickListener(AdapterView.OnItemClickListener onItemClickListener) { + public void setOnPlayClickListener(AdapterView.OnItemClickListener onItemClickListener) { this.onItemClickListener = onItemClickListener; } @@ -145,7 +145,7 @@ static class ViewHolder { TextView caseName; TextView caseDesc; TextView createTime; - RelativeLayout edit; + RelativeLayout play; } } diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseAdapter.java new file mode 100644 index 0000000..42bea99 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseAdapter.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.widget.BaseAdapter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by lezhou.wyl on 2018/8/15. + */ + +public abstract class SoloBaseAdapter extends BaseAdapter { + + protected LayoutInflater mInflater; + protected Context mContext; + protected List mData = new ArrayList<>(); + + public SoloBaseAdapter(Context context) { + mContext = context; + mInflater = LayoutInflater.from(context); + } + + @Override + public int getCount() { + return mData.size(); + } + + @Override + public T getItem(int position) { + return mData.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + public void setData(List data) { + if (data == null) { + mData = new ArrayList<>(); + } else { + mData = new ArrayList<>(data); + } + notifyDataSetChanged(); + } + + public List getData() { + List result = new ArrayList<>(); + if (mData != null) { + result.addAll(mData); + } + + return result; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseRecyclerAdapter.java b/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseRecyclerAdapter.java new file mode 100644 index 0000000..37e5644 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/adapter/SoloBaseRecyclerAdapter.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.alipay.hulu.common.utils.LogUtil; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * 通用Adapter对象 + * @param + */ +public abstract class SoloBaseRecyclerAdapter extends RecyclerView.Adapter> { + private static final String TAG = SoloBaseRecyclerAdapter.class.getSimpleName(); + protected List dataList = new ArrayList<>(); + protected Context context; + protected LayoutInflater inflater; + protected OnItemClickListener listener; + private int layoutId; + + protected View.OnClickListener itemsOnClickListener; + protected View.OnLongClickListener itemsOnLongClickListener; + + public SoloBaseRecyclerAdapter(Context context, @LayoutRes int layoutId) { + this.context = context; + this.inflater = LayoutInflater.from(context); + this.layoutId = layoutId; + + this.itemsOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Integer position = (Integer) v.getTag(); + if (listener != null && position != null && position >= 0 && position < dataList.size()) { + T data = dataList.get(position); + listener.onItemClick(data, position); + } + } + }; + + this.itemsOnLongClickListener = new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + Integer position = (Integer) v.getTag(); + if (listener != null && position != null && position >= 0 && position < dataList.size()) { + T data = dataList.get(position); + return listener.onItemLongClick(data, position); + } + + return false; + } + }; + + } + + /** + * 更新数据 + * @param newData + */ + public void updateDate(List newData) { + dataList.clear(); + if (newData != null) { + dataList.addAll(newData); + } + notifyDataSetChanged(); + } + + /** + * 添加数据 + * @param data + */ + public void addItem(T data) { + if (data != null) { + dataList.add(data); + notifyItemInserted(dataList.size()); + } + } + + /** + * 删除对象 + * @param index + */ + public void deleteItem(int index) { + dataList.remove(index); + notifyItemRemoved(index); + } + + /** + * 获取对象数量 + * @return + */ + public int getCount() { + return dataList.size(); + } + + /** + * 获取所有对象 + * @return + */ + public List getAllData() { + return new ArrayList<>(dataList); + } + + /** + * 生成ViewHolder + * @param view + * @return + */ + public abstract SimpleViewHolder generateViewHolder(View view); + + @NonNull + @Override + public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = inflater.inflate(layoutId, parent, false); + + // 监听点击事件 + registerListener(v); + SimpleViewHolder holder = generateViewHolder(v); + holder.setRef(this); + return holder; + } + + private void registerListener(View v) { + v.setOnClickListener(itemsOnClickListener); + v.setOnLongClickListener(itemsOnLongClickListener); + } + + @Override + public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position) { + T data = dataList.get(position); + holder._bindData(data, position); + } + + @Override + public int getItemCount() { + return dataList.size(); + } + + /** + * 设置事件监听器 + * @param listener + */ + public void setItemOperationListener(OnItemClickListener listener) { + this.listener = listener; + } + + /** + * 简易ViewHolder对象 + * @param + */ + public static abstract class SimpleViewHolder extends RecyclerView.ViewHolder { + protected WeakReference> ref; + public SimpleViewHolder(@NonNull View itemView) { + super(itemView); + bindView(itemView); + } + + public void setRef(SoloBaseRecyclerAdapter adapter) { + this.ref = new WeakReference<>(adapter); + } + + /** + * 删除自身数据 + */ + public void deleteSelf() { + if (ref == null || ref.get() == null) { + LogUtil.w(TAG, "Holder ref is null"); + return; + } + + ref.get().deleteItem((Integer) itemView.getTag()); + } + + /** + * 绑定View对象 + * @param base + */ + public abstract void bindView(View base); + + public void _bindData(K data, int position) { + itemView.setTag(position); + bindData(data, position); + } + + /** + * 绑定数据 + * @param data + */ + public abstract void bindData(K data, int index); + } + + public interface OnItemClickListener { + void onItemClick(K data, int position); + boolean onItemLongClick(K data, int position); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/bean/AdvanceCaseSetting.java b/src/app/src/main/java/com/alipay/hulu/bean/AdvanceCaseSetting.java index 1597742..853fcfc 100644 --- a/src/app/src/main/java/com/alipay/hulu/bean/AdvanceCaseSetting.java +++ b/src/app/src/main/java/com/alipay/hulu/bean/AdvanceCaseSetting.java @@ -15,6 +15,10 @@ */ package com.alipay.hulu.bean; +import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; + +import java.util.List; + /** * Created by lezhou.wyl on 2018/7/16. */ @@ -22,6 +26,34 @@ public class AdvanceCaseSetting { private String descriptorMode; private int version; + private List params; + private String overrideApp; + private CaseRunningParam runningParam; + + /** + * 准备步骤(不录制) + */ + private List prepareActions; + + /** + * 后续步骤(不录制) + */ + private List suffixActions; + + public AdvanceCaseSetting() { + + } + + public AdvanceCaseSetting(AdvanceCaseSetting old) { + if (old == null) { + return; + } + this.overrideApp = old.overrideApp; + this.descriptorMode = old.descriptorMode; + this.params = old.params; + this.runningParam = old.runningParam; + this.version = old.version; + } public String getDescriptorMode() { return descriptorMode; @@ -38,4 +70,44 @@ public int getVersion() { public void setVersion(int version) { this.version = version; } + + public String getOverrideApp() { + return overrideApp; + } + + public void setOverrideApp(String overrideApp) { + this.overrideApp = overrideApp; + } + + public List getParams() { + return params; + } + + public void setParams(List params) { + this.params = params; + } + + public CaseRunningParam getRunningParam() { + return runningParam; + } + + public void setRunningParam(CaseRunningParam runningParam) { + this.runningParam = runningParam; + } + + public List getPrepareActions() { + return prepareActions; + } + + public void setPrepareActions(List prepareActions) { + this.prepareActions = prepareActions; + } + + public List getSuffixActions() { + return suffixActions; + } + + public void setSuffixActions(List suffixActions) { + this.suffixActions = suffixActions; + } } \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/bean/CaseParamBean.java b/src/app/src/main/java/com/alipay/hulu/bean/CaseParamBean.java new file mode 100644 index 0000000..663d0ca --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/bean/CaseParamBean.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.bean; + +public class CaseParamBean { + /** + * 参数名称 + */ + private String paramName; + + /** + * 参数描述 + */ + private String paramDesc; + + /** + * 参数默认值 + */ + private String paramDefaultValue; + + public String getParamName() { + return paramName; + } + + public void setParamName(String paramName) { + this.paramName = paramName; + } + + public String getParamDesc() { + return paramDesc; + } + + public void setParamDesc(String paramDesc) { + this.paramDesc = paramDesc; + } + + public String getParamDefaultValue() { + return paramDefaultValue; + } + + public void setParamDefaultValue(String paramDefaultValue) { + this.paramDefaultValue = paramDefaultValue; + } + + @Override + public String toString() { + return "CaseParamBean{" + + "paramName='" + paramName + '\'' + + ", paramDesc='" + paramDesc + '\'' + + ", paramDefaultValue='" + paramDefaultValue + '\'' + + '}'; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/bean/CaseRunningParam.java b/src/app/src/main/java/com/alipay/hulu/bean/CaseRunningParam.java new file mode 100644 index 0000000..d5f76a8 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/bean/CaseRunningParam.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.bean; + +import com.alibaba.fastjson.JSONObject; + +import java.util.List; + +/** + * Created by qiaoruikai on 2019-08-19 21:05. + */ +public class CaseRunningParam { + private ParamMode mode; + private List paramList; + + public ParamMode getMode() { + return mode; + } + + public void setMode(ParamMode mode) { + this.mode = mode; + } + + public List getParamList() { + return paramList; + } + + public void setParamList(List paramList) { + this.paramList = paramList; + } + + /** + * 可选模式 + */ + public enum ParamMode { + SEPARATE, + UNION + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/bean/CaseStepHolder.java b/src/app/src/main/java/com/alipay/hulu/bean/CaseStepHolder.java index 5009937..f7729b1 100644 --- a/src/app/src/main/java/com/alipay/hulu/bean/CaseStepHolder.java +++ b/src/app/src/main/java/com/alipay/hulu/bean/CaseStepHolder.java @@ -16,8 +16,12 @@ package com.alipay.hulu.bean; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -27,6 +31,7 @@ public class CaseStepHolder { private static Map caseHolder = new HashMap<>(); private static Map replayHolder = new HashMap<>(); + private static List pasteContentHolder; private static final AtomicInteger counter = new AtomicInteger(1); private static final AtomicInteger replayCounter = new AtomicInteger(1); @@ -42,6 +47,34 @@ public static int storeCase(RecordCaseInfo caseInfo) { return id; } + /** + * 暂存拷贝步骤 + * @param pasteContent + */ + public static void storePasteContent(List pasteContent) { + pasteContentHolder = new ArrayList<>(pasteContent); + } + + /** + * 获取并置空拷贝步骤 + * @return + */ + public static List getPasteContent() { + if (pasteContentHolder == null) { + return Collections.EMPTY_LIST; + } + + return new ArrayList<>(pasteContentHolder); + } + + /** + * 是否包含拷贝步骤 + * @return + */ + public static boolean containsPasteContent() { + return pasteContentHolder != null; + } + /** * 获取用例 * @param id diff --git a/src/app/src/main/java/com/alipay/hulu/bean/CaseStepStatus.java b/src/app/src/main/java/com/alipay/hulu/bean/CaseStepStatus.java new file mode 100644 index 0000000..f3d9d7c --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/bean/CaseStepStatus.java @@ -0,0 +1,46 @@ +package com.alipay.hulu.bean; + +import androidx.annotation.StringRes; + +import com.alipay.hulu.R; +import com.alipay.hulu.common.utils.StringUtil; + +/** + * Created by qiaoruikai on 2019/12/18 2:29 PM. + */ +public enum CaseStepStatus { + FINISH("finish", R.string.case_step_status__finish), + FAIL("fail", R.string.case_step_status__fail), + UNENFORCED("unenforced", R.string.case_step_status__unenforced), + ; + private String code; + private int desc; + + CaseStepStatus(String code, @StringRes int desc) { + this.code = code; + this.desc = desc; + } + + /** + * 根据Code查找 + * @param code + * @return + */ + public static CaseStepStatus getByCode(String code) { + for (CaseStepStatus status: values()) { + if (status.code.equals(code)) { + return status; + } + } + + return null; + } + + public String getCode() { + return code; + } + + public String getName() { + return StringUtil.getString(desc); + } +} diff --git a/src/shared/src/main/java/com/alipay/hulu/shared/io/constant/Constant.java b/src/app/src/main/java/com/alipay/hulu/bean/OperationStepResult.java similarity index 64% rename from src/shared/src/main/java/com/alipay/hulu/shared/io/constant/Constant.java rename to src/app/src/main/java/com/alipay/hulu/bean/OperationStepResult.java index ef79570..1229428 100644 --- a/src/shared/src/main/java/com/alipay/hulu/shared/io/constant/Constant.java +++ b/src/app/src/main/java/com/alipay/hulu/bean/OperationStepResult.java @@ -13,14 +13,32 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.alipay.hulu.shared.io.constant; +package com.alipay.hulu.bean; + + +import java.io.File; /** - * Created by qiaoruikai on 2018/10/10 8:54 PM. + * 步骤执行结果 */ -public class Constant { +public class OperationStepResult { + /** + * 操作步骤信息 + */ + public String method; + + /** + * 失败原因 + */ + public String error; + + /** + * 执行结果 + */ + public boolean result; + /** - * 通知记录操作步骤 + * 结果截图 */ - public static final String NOTIFY_RECORD_STEP = "notifyRecordStep"; -} + public File screenCaptureFile; +} \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/bean/ReplayResultBean.java b/src/app/src/main/java/com/alipay/hulu/bean/ReplayResultBean.java index 988f264..f8e0dd5 100644 --- a/src/app/src/main/java/com/alipay/hulu/bean/ReplayResultBean.java +++ b/src/app/src/main/java/com/alipay/hulu/bean/ReplayResultBean.java @@ -18,8 +18,12 @@ import android.os.Parcel; import android.os.Parcelable; +import com.alibaba.fastjson.JSON; +import com.alipay.hulu.common.bean.DeviceInfo; +import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; +import java.io.File; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -41,6 +45,16 @@ public class ReplayResultBean implements Parcelable { */ private String targetApp; + /** + * 目标应用包名 + */ + private String targetAppPkg; + + /** + * 目标应用版本号 + */ + private String targetAppVersion; + /** * 开始时间 */ @@ -76,11 +90,38 @@ public class ReplayResultBean implements Parcelable { */ private int exceptionStep; + /** + * 故障步骤ID + */ + private String exceptionStepId; + + private DeviceInfo deviceInfo; + + /** + * 平台 + */ + private String platform; + + /** + * 平台版本 + */ + private String platformVersion; + /** * 截图文件 */ private Map screenshotFiles; + /** + * (仅用于本地结果)结果截图列表 + */ + private List screenshots; + + /** + * (仅用于本地结果)结果目录 + */ + private File baseDir; + public String getCaseName() { return caseName; } @@ -97,6 +138,22 @@ public void setTargetApp(String targetApp) { this.targetApp = targetApp; } + public String getTargetAppPkg() { + return targetAppPkg; + } + + public void setTargetAppPkg(String targetAppPkg) { + this.targetAppPkg = targetAppPkg; + } + + public String getTargetAppVersion() { + return targetAppVersion; + } + + public void setTargetAppVersion(String targetAppVersion) { + this.targetAppVersion = targetAppVersion; + } + public Date getStartTime() { return startTime; } @@ -137,6 +194,22 @@ public void setCurrentOperationLog(List currentOperationLog) { this.currentOperationLog = currentOperationLog; } + public List getScreenshots() { + return screenshots; + } + + public void setScreenshots(List screenshots) { + this.screenshots = screenshots; + } + + public File getBaseDir() { + return baseDir; + } + + public void setBaseDir(File baseDir) { + this.baseDir = baseDir; + } + public String getExceptionMessage() { return exceptionMessage; } @@ -153,6 +226,14 @@ public void setExceptionStep(int exceptionStep) { this.exceptionStep = exceptionStep; } + public String getExceptionStepId() { + return exceptionStepId; + } + + public void setExceptionStepId(String exceptionStepId) { + this.exceptionStepId = exceptionStepId; + } + public Map getScreenshotFiles() { return screenshotFiles; } @@ -161,6 +242,30 @@ public void setScreenshotFiles(Map screenshotFiles) { this.screenshotFiles = screenshotFiles; } + public DeviceInfo getDeviceInfo() { + return deviceInfo; + } + + public void setDeviceInfo(DeviceInfo deviceInfo) { + this.deviceInfo = deviceInfo; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getPlatformVersion() { + return platformVersion; + } + + public void setPlatformVersion(String platformVersion) { + this.platformVersion = platformVersion; + } + public ReplayResultBean() {} @Override @@ -175,6 +280,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeLong(this.endTime != null ? this.endTime.getTime() : -1); dest.writeString(this.logFile); dest.writeString(this.targetApp); + dest.writeString(this.targetAppPkg); + dest.writeString(this.targetAppVersion); if (this.actionLogs == null) { dest.writeInt(-1); } else { @@ -187,6 +294,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeList(this.currentOperationLog); dest.writeString(this.exceptionMessage); dest.writeInt(this.exceptionStep); + dest.writeString(this.exceptionStepId); if (this.screenshotFiles == null) { dest.writeInt(-1); } else { @@ -196,6 +304,14 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(entry.getValue()); } } + DeviceInfo deviceInfo = this.deviceInfo; + if (deviceInfo == null) { + dest.writeString(""); + } else { + dest.writeString(JSON.toJSONString(deviceInfo)); + } + dest.writeString(this.platform); + dest.writeString(this.platformVersion); } protected ReplayResultBean(Parcel in) { @@ -206,6 +322,8 @@ protected ReplayResultBean(Parcel in) { this.endTime = tmpEndTime == -1 ? null : new Date(tmpEndTime); this.logFile = in.readString(); this.targetApp = in.readString(); + this.targetAppPkg = in.readString(); + this.targetAppVersion = in.readString(); int actionLogsSize = in.readInt(); if (actionLogsSize > -1) { this.actionLogs = new HashMap<>(actionLogsSize); @@ -218,6 +336,7 @@ protected ReplayResultBean(Parcel in) { this.currentOperationLog = in.readArrayList(OperationStep.class.getClassLoader()); this.exceptionMessage = in.readString(); this.exceptionStep = in.readInt(); + this.exceptionStepId = in.readString(); int screenshotFilesSize = in.readInt(); if (screenshotFilesSize > -1) { this.screenshotFiles = new HashMap<>(screenshotFilesSize + 1); @@ -227,6 +346,12 @@ protected ReplayResultBean(Parcel in) { this.screenshotFiles.put(key, value); } } + String deviceInfo = in.readString(); + if (!StringUtil.isEmpty(deviceInfo)) { + this.deviceInfo = JSON.parseObject(deviceInfo, DeviceInfo.class); + } + this.platform = in.readString(); + this.platformVersion = in.readString(); } public static final Creator CREATOR = new Creator() { diff --git a/src/app/src/main/java/com/alipay/hulu/bean/ScreenshotBean.java b/src/app/src/main/java/com/alipay/hulu/bean/ScreenshotBean.java new file mode 100644 index 0000000..2dce8e6 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/bean/ScreenshotBean.java @@ -0,0 +1,25 @@ +package com.alipay.hulu.bean; + +/** + * 截图信息 + */ +public class ScreenshotBean { + private String name; + private String file; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/event/ScanSuccessEvent.java b/src/app/src/main/java/com/alipay/hulu/event/ScanSuccessEvent.java index 5e3ecad..b4ab3d8 100644 --- a/src/app/src/main/java/com/alipay/hulu/event/ScanSuccessEvent.java +++ b/src/app/src/main/java/com/alipay/hulu/event/ScanSuccessEvent.java @@ -18,6 +18,10 @@ import android.os.Parcel; import android.os.Parcelable; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.scan.ScanCodeType; +import com.google.zxing.BarcodeFormat; + /** * Created by lezhou.wyl on 2018/2/7. */ @@ -25,8 +29,13 @@ public class ScanSuccessEvent implements Parcelable { public static final int SCAN_TYPE_SCHEME = 1; + public static final int SCAN_TYPE_PARAM = 6; + public static final int SCAN_TYPE_OTHER = 7; + public static final int SCAN_TYPE_QR_CODE = 10; + public static final int SCAN_TYPE_BAR_CODE = 11; private int type; private String content; + private ScanCodeType codeType; public int getType() { return type; @@ -44,6 +53,13 @@ public void setContent(String content) { this.content = content; } + public ScanCodeType getCodeType() { + return codeType; + } + + public void setCodeType(ScanCodeType codeType) { + this.codeType = codeType; + } @Override public int describeContents() { @@ -54,6 +70,9 @@ public int describeContents() { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.type); dest.writeString(this.content); + if (this.codeType != null) { + dest.writeString(this.codeType.getCode()); + } } public ScanSuccessEvent() { @@ -62,6 +81,10 @@ public ScanSuccessEvent() { protected ScanSuccessEvent(Parcel in) { this.type = in.readInt(); this.content = in.readString(); + String code = in.readString(); + if (StringUtil.isNotEmpty(code)) { + this.codeType = ScanCodeType.getByCode(code); + } } public static final Parcelable.Creator CREATOR = new Creator() { diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/BaseFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/BaseFragment.java index 4367142..e529bbf 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/BaseFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/BaseFragment.java @@ -19,8 +19,8 @@ import android.app.ProgressDialog; import android.content.Context; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import android.text.TextUtils; import android.view.View; import android.view.inputmethod.InputMethodManager; @@ -95,11 +95,10 @@ public void toastShort(final String msg) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { - if (toast == null) { - toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_SHORT); - } else { - toast.setText(msg); + if (toast != null) { + toast.cancel(); } + toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_SHORT); toast.show(); } }); @@ -133,11 +132,10 @@ public void toastLong(final String msg) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { - if (toast == null) { - toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_LONG); - } else { - toast.setText(msg); + if (toast != null) { + toast.cancel(); } + toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_LONG); toast.show(); } }); diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/BatchExecutionFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/BatchExecutionFragment.java index 017a966..c7b5ec1 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/BatchExecutionFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/BatchExecutionFragment.java @@ -16,33 +16,22 @@ package com.alipay.hulu.fragment; import android.os.Bundle; -import android.provider.Settings; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.CompoundButton; import android.widget.ListView; import android.widget.TextView; -import android.widget.Toast; import com.alipay.hulu.R; -import com.alipay.hulu.activity.BaseActivity; import com.alipay.hulu.activity.BatchExecutionActivity; -import com.alipay.hulu.activity.MyApplication; import com.alipay.hulu.adapter.BatchExecutionListAdapter; -import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.tools.BackgroundExecutor; -import com.alipay.hulu.common.utils.PermissionUtil; -import com.alipay.hulu.replay.BatchStepProvider; -import com.alipay.hulu.service.CaseReplayManager; +import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; import com.alipay.hulu.shared.io.db.GreenDaoManager; import com.alipay.hulu.shared.io.db.RecordCaseInfoDao; -import java.util.Arrays; import java.util.List; /** @@ -66,7 +55,7 @@ public static int[] getTypes() { } public static String getTypeName(int type) { - return "本地"; + return StringUtil.getString(R.string.replay_list__local); } public static BatchExecutionFragment newInstance(int type) { @@ -143,10 +132,6 @@ private void initListView(View view) { private void initEmptyView(View view) { mEmptyView = view.findViewById(R.id.empty_view_container); mEmptyTextView = (TextView) view.findViewById(R.id.empty_text); - mEmptyTextView.setText("没有发现用例"); - } - - private void showEnableAccessibilityServiceHint() { - Toast.makeText(getContext(), "请在辅助功能中开启Soloπ", Toast.LENGTH_LONG).show(); + mEmptyTextView.setText(R.string.batch__no_case); } } diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/CaseDescEditFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/CaseDescEditFragment.java index 75e9bb4..8d68b79 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/CaseDescEditFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/CaseDescEditFragment.java @@ -16,33 +16,48 @@ package com.alipay.hulu.fragment; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.EditText; +import android.widget.ListView; import com.alibaba.fastjson.JSON; +import com.alipay.hulu.adapter.ParamListAdapter; import com.alipay.hulu.R; import com.alipay.hulu.activity.CaseEditActivity; import com.alipay.hulu.bean.AdvanceCaseSetting; +import com.alipay.hulu.bean.CaseParamBean; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.RunningThread; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.utils.LogUtil; -import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import java.util.List; + public class CaseDescEditFragment extends BaseFragment implements CaseEditActivity.OnCaseSaveListener { private static final String TAG = "CaseStepEditFrag"; - public static final String RECORD_CASE_EXTRA = "record_case"; - private RecordCaseInfo mRecordCase; + private AdvanceCaseSetting setting; + private EditText mCaseName; private EditText mCaseDesc; + private ListView mParams; - // 用例版本号 - private int caseVersion = 0; + private ParamListAdapter adapter; + + @Subscriber(value = @Param(sticky = false), thread = RunningThread.MAIN_THREAD) + public void receiveNewParam(CaseParamBean param) { + List paramBeanList = adapter.getData(); + paramBeanList.add(param); + adapter.setData(paramBeanList); + } /** * 通过RecordCase初始化 @@ -62,6 +77,7 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, // 获取各项控件 mCaseName = (EditText) root.findViewById(R.id.case_name); mCaseDesc = (EditText) root.findViewById(R.id.case_desc); + mParams = (ListView) root.findViewById(R.id.case_params); return root; } @@ -83,12 +99,17 @@ private void initData() { mCaseName.setText(mRecordCase.getCaseName()); mCaseDesc.setText(mRecordCase.getCaseDesc()); + setting = JSON.parseObject(mRecordCase.getAdvanceSettings() + , AdvanceCaseSetting.class); + + // 参数列表 + adapter = new ParamListAdapter(getActivity()); + mParams.setAdapter(adapter); - // 如果有高级设置 - if (!StringUtil.isEmpty(mRecordCase.getAdvanceSettings())) { - AdvanceCaseSetting setting = JSON.parseObject(mRecordCase.getAdvanceSettings(), - AdvanceCaseSetting.class); - caseVersion = setting.getVersion(); + if (setting != null) { + adapter.setData(setting.getParams()); + } else { + mParams.setVisibility(View.GONE); } } @@ -97,9 +118,24 @@ public void onCaseSave() { mRecordCase.setCaseName(mCaseName.getText().toString()); mRecordCase.setCaseDesc(mCaseDesc.getText().toString()); - AdvanceCaseSetting advanceCaseSetting = new AdvanceCaseSetting(); - advanceCaseSetting.setVersion(caseVersion); + if (setting == null) { + setting = new AdvanceCaseSetting(); + } + if (adapter != null && mParams.getVisibility() == View.VISIBLE) { + setting.setParams(adapter.getData()); + } + mRecordCase.setAdvanceSettings(JSON.toJSONString(setting)); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + InjectorService.g().register(this); + } - mRecordCase.setAdvanceSettings(JSON.toJSONString(advanceCaseSetting)); + @Override + public void onDestroy() { + super.onDestroy(); + InjectorService.g().unregister(this); } } diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/CaseParamSeparateFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/CaseParamSeparateFragment.java new file mode 100644 index 0000000..dacb108 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/fragment/CaseParamSeparateFragment.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.fragment; + +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; + +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.R; +import com.alipay.hulu.activity.CaseParamEditActivity; +import com.alipay.hulu.bean.AdvanceCaseSetting; +import com.alipay.hulu.bean.CaseParamBean; +import com.alipay.hulu.bean.CaseRunningParam; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by qiaoruikai on 2019-08-19 23:37. + */ +public class CaseParamSeparateFragment extends CaseParamEditActivity.CaseParamFragment { + private static final String TAG = "CaseParamSeparateFragment"; + private ListView paramList; + + // 用例参数设置 + private List presetParams; + private List holders; + private CaseRunningParam runningParam; + private Map storedParams; + private ParamHolder waitingHolder; + + /** + * 设置高级设置 + * + * @param advanceCaseSetting + */ + @Override + public void setAdvanceCaseSetting(@NonNull AdvanceCaseSetting advanceCaseSetting) { + storedParams = new LinkedHashMap<>(); + presetParams = advanceCaseSetting.getParams(); + runningParam = advanceCaseSetting.getRunningParam(); + if (runningParam == null) { + runningParam = new CaseRunningParam(); + } + + // 如果之前有存储p + if (runningParam.getMode() == CaseRunningParam.ParamMode.SEPARATE) { + List params = runningParam.getParamList(); + if (params != null) { + for (JSONObject obj: params) { + for (String key: obj.keySet()) { + storedParams.put(key, obj.getString(key)); + } + } + } + } + } + + @Override + public CaseRunningParam getRunningParam() { + int count = paramList.getCount(); + List params = new ArrayList<>(count + 1); + for (String key: storedParams.keySet()) { + JSONObject paramInfo = new JSONObject(2); + paramInfo.put(key, storedParams.get(key)); + params.add(paramInfo); + } + LogUtil.d(TAG,"message:" + params); + + runningParam.setMode(CaseRunningParam.ParamMode.SEPARATE); + runningParam.setParamList(params); + return runningParam; + } + + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.dialog_param_edit, container, false); + paramList = (ListView) root.findViewById(R.id.dialog_param_list); + + return root; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (presetParams != null) { + holders = new ArrayList<>(presetParams.size() + 1); + for (CaseParamBean param : presetParams) { + ParamHolder holder = new ParamHolder(); + holder.param = param; + holders.add(holder); + } + + final LayoutInflater inflater = LayoutInflater.from(getActivity()); + + paramList.setAdapter(new BaseAdapter() { + @Override + public int getCount() { + return holders.size(); + } + + @Override + public Object getItem(int position) { + return holders.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.item_case_step_edit_input, parent, false); + convertView.findViewById(R.id.item_case_step_create_param).setVisibility(View.GONE); + } + TextView title = (TextView) convertView.findViewById(R.id.item_case_step_name); + final EditText edit = (EditText) convertView.findViewById(R.id.item_case_step_edit); + + // 移除旧的textWatcher + TextWatcher oldTextWatcher = (TextWatcher) edit.getTag(); + if (oldTextWatcher != null) { + edit.removeTextChangedListener(oldTextWatcher); + } + + final ParamHolder holder = (ParamHolder) getItem(position); + final CaseParamBean paramBean = holder.param; + String desc = StringUtil.isEmpty(paramBean.getParamDesc()) ? paramBean.getParamName() : paramBean.getParamDesc(); + + String defaultValue = storedParams.get(paramBean.getParamName()); + if (defaultValue == null) { + defaultValue = ""; + } + edit.setText(defaultValue); + TextWatcher textWatcher = new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + storedParams.put(paramBean.getParamName(), s.toString()); + } + + @Override + public void afterTextChanged(Editable s) { + + } + }; + edit.setTag(textWatcher); + edit.addTextChangedListener(textWatcher); + + title.setText(desc); + + return convertView; + } + }); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + LogUtil.d(TAG, "On activity result: %d, %d, %s", requestCode, resultCode, data); + } + + private static class ParamHolder { + private CaseParamBean param; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/CaseParamUnionFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/CaseParamUnionFragment.java new file mode 100644 index 0000000..6647821 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/fragment/CaseParamUnionFragment.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.fragment; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; + +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.R; +import com.alipay.hulu.activity.CaseParamEditActivity; +import com.alipay.hulu.bean.AdvanceCaseSetting; +import com.alipay.hulu.bean.CaseParamBean; +import com.alipay.hulu.bean.CaseRunningParam; +import com.alipay.hulu.common.utils.StringUtil; +import com.zhy.view.flowlayout.FlowLayout; +import com.zhy.view.flowlayout.TagAdapter; +import com.zhy.view.flowlayout.TagFlowLayout; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by qiaoruikai on 2019-08-19 22:25. + */ +public class CaseParamUnionFragment extends CaseParamEditActivity.CaseParamFragment { + private TagFlowLayout tagFlowLayout; + private ListView paramList; + private Button addBtn; + + // 用例参数设置 + private List presetParams; + private CaseRunningParam runningParam; + private List storedParams; + + + /** + * 设置高级设置 + * + * @param advanceCaseSetting + */ + @Override + public void setAdvanceCaseSetting(@NonNull AdvanceCaseSetting advanceCaseSetting) { + storedParams = null; + presetParams = advanceCaseSetting.getParams(); + if (presetParams == null) { + presetParams = new ArrayList<>(); + } + + runningParam = advanceCaseSetting.getRunningParam(); + if (runningParam == null) { + runningParam = new CaseRunningParam(); + } + if (presetParams == null) { + presetParams = new ArrayList<>(); + } + + // 如果之前有存储p + if (runningParam.getMode() == CaseRunningParam.ParamMode.UNION) { + storedParams = new ArrayList<>(runningParam.getParamList()); + } + + if (storedParams == null) { + storedParams = new ArrayList<>(); + } + } + + @Override + public CaseRunningParam getRunningParam() { + runningParam.setMode(CaseRunningParam.ParamMode.UNION); + runningParam.setParamList(storedParams); + return runningParam; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_union_param, container, false); + tagFlowLayout = (TagFlowLayout) root.findViewById(R.id.union_param_group); + paramList = (ListView) root.findViewById(R.id.union_param_list); + addBtn = (Button) root.findViewById(R.id.union_param_add); + + return root; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + final LayoutInflater inflater = LayoutInflater.from(getActivity()); + + tagFlowLayout.setAdapter(new TagAdapter(storedParams) { + @Override + public View getView(FlowLayout parent, int position, JSONObject o) { + View root = inflater.inflate(R.layout.item_param_info, parent, false); + List diffParams = new ArrayList<>(); + for (CaseParamBean paramBean: presetParams) { + diffParams.add(o.getString(paramBean.getParamName())); + } + + TextView title = (TextView) root.findViewById(R.id.batch_execute_tag_name); + title.setText(StringUtil.join(",", diffParams)); + return root; + } + }); + tagFlowLayout.setOnTagClickListener(new TagFlowLayout.OnTagClickListener() { + @Override + public boolean onTagClick(View view, int position, FlowLayout parent) { + storedParams.remove(position); + tagFlowLayout.getAdapter().notifyDataChanged(); + return true; + } + }); + + final List holders = new ArrayList<>(); + for (CaseParamBean param: presetParams) { + ParamHolder holder = new ParamHolder(); + holder.param = param; + holders.add(holder); + } + paramList.setAdapter(new BaseAdapter() { + @Override + public int getCount() { + return holders.size(); + } + + @Override + public Object getItem(int position) { + return holders.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = inflater.inflate(R.layout.item_case_step_edit_input, parent, false); + convertView.findViewById(R.id.item_case_step_create_param).setVisibility(View.GONE); + } + TextView title = (TextView) convertView.findViewById(R.id.item_case_step_name); + EditText edit = (EditText) convertView.findViewById(R.id.item_case_step_edit); + + ParamHolder holder = (ParamHolder) getItem(position); + CaseParamBean paramBean = holder.param; + String desc = StringUtil.isEmpty(paramBean.getParamDesc())? paramBean.getParamName(): paramBean.getParamDesc(); + + title.setText(desc); + holder.edit = edit; + + return convertView; + } + }); + + addBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + JSONObject obj = new JSONObject(holders.size() + 1); + for (ParamHolder holder : holders) { + obj.put(holder.param.getParamName(), holder.edit.getText().toString()); + holder.edit.setText(""); + } + + storedParams.add(obj); + ((BaseAdapter)paramList.getAdapter()).notifyDataSetChanged(); + tagFlowLayout.getAdapter().notifyDataChanged(); + } + }); + } + + private static class ParamHolder { + private CaseParamBean param; + private EditText edit; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java index 50bbfce..8f167a4 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java @@ -24,11 +24,14 @@ import android.os.Build; import android.os.Bundle; import android.os.Parcel; -import android.support.annotation.Nullable; -import android.support.design.widget.TabLayout; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import android.provider.Settings; +import androidx.annotation.Nullable; + +import com.alipay.hulu.shared.scan.ScanCodeType; +import com.google.android.material.tabs.TabLayout; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; @@ -37,17 +40,18 @@ import android.view.animation.AnimationUtils; import android.view.animation.LayoutAnimationController; import android.widget.AdapterView; +import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import com.alibaba.fastjson.JSON; import com.alipay.hulu.R; import com.alipay.hulu.activity.CaseEditActivity; -import com.alipay.hulu.activity.NewRecordActivity; import com.alipay.hulu.activity.QRScanActivity; import com.alipay.hulu.adapter.CaseStepAdapter; import com.alipay.hulu.adapter.CaseStepMethodAdapter; import com.alipay.hulu.adapter.CaseStepNodeAdapter; +import com.alipay.hulu.bean.CaseStepHolder; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.injector.param.RunningThread; @@ -56,20 +60,27 @@ import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.event.ScanSuccessEvent; +import com.alipay.hulu.service.CaseRecordManager; +import com.alipay.hulu.shared.io.OperationStepService; import com.alipay.hulu.shared.io.bean.GeneralOperationLogBean; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; -import com.alipay.hulu.shared.io.db.GreenDaoManager; import com.alipay.hulu.shared.io.util.OperationStepUtil; import com.alipay.hulu.shared.node.action.OperationExecutor; import com.alipay.hulu.shared.node.action.OperationMethod; import com.alipay.hulu.shared.node.action.PerformActionEnum; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; +import com.alipay.hulu.shared.node.tree.OperationNode; +import com.alipay.hulu.shared.node.tree.accessibility.tree.AccessibilityNodeTree; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; +import com.alipay.hulu.shared.node.utils.AppUtil; import com.alipay.hulu.shared.node.utils.LogicUtil; +import com.alipay.hulu.shared.node.utils.PrepareUtil; import com.alipay.hulu.ui.MaxHeightScrollView; import com.alipay.hulu.ui.TwoLevelSelectLayout; +import com.alipay.hulu.util.CaseAppendOperationProcessor; import com.alipay.hulu.util.FunctionSelectUtil; import com.yydcdut.sdlv.Menu; import com.yydcdut.sdlv.MenuItem; @@ -78,11 +89,8 @@ import com.zhy.view.flowlayout.TagAdapter; import com.zhy.view.flowlayout.TagFlowLayout; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -94,8 +102,12 @@ public class CaseStepEditFragment extends BaseFragment implements TagFlowLayout. private static final String TAG = "CaseStepEditFragment"; private boolean isOverrideInstall = false; + private boolean selectMode = false; + private RecordCaseInfo recordCase; + private int tmpPosition = -1; + private TagFlowLayout tagGroup; private SlideAndDragListView dragList; @@ -156,12 +168,70 @@ public void onScanEvent(final ScanSuccessEvent event) { CaseStepAdapter.MyDataWrapper wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); - dragEntities.add(wrapper); + if (tmpPosition != -1) { + dragEntities.add(tmpPosition, wrapper); + tmpPosition = -1; + } else { + dragEntities.add(wrapper); + } adapter.notifyDataSetChanged(); + break; + case ScanSuccessEvent.SCAN_TYPE_PARAM: + // 向handler发送请求 + method = new OperationMethod(PerformActionEnum.LOAD_PARAM); + method.putParam(OperationExecutor.APP_URL_KEY, event.getContent()); + step = new OperationStep(); + step.setOperationMethod(method); + step.setOperationIndex(currentIdx.get()); + step.setOperationId(stepList.get(stepList.size() - 1).getOperationId()); + + wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); + + if (tmpPosition != -1) { + dragEntities.add(tmpPosition, wrapper); + tmpPosition = -1; + } else { + dragEntities.add(wrapper); + } + + adapter.notifyDataSetChanged(); + + // 录制模式需要记录下 + + break; + + case ScanSuccessEvent.SCAN_TYPE_QR_CODE: + case ScanSuccessEvent.SCAN_TYPE_BAR_CODE: + // 向handler发送请求 + method = new OperationMethod(event.getType() == ScanSuccessEvent.SCAN_TYPE_QR_CODE? + PerformActionEnum.GENERATE_QR_CODE: PerformActionEnum.GENERATE_BAR_CODE); + method.putParam(OperationExecutor.SCHEME_KEY, event.getContent()); + if (event.getType() == ScanSuccessEvent.SCAN_TYPE_BAR_CODE) { + ScanCodeType type = event.getCodeType(); + if (type != null) { + method.putParam(OperationExecutor.GENERATE_CODE_TYPE, type.getCode()); + } + } // 录制模式需要记录下 + step = new OperationStep(); + step.setOperationMethod(method); + step.setOperationIndex(currentIdx.get()); + step.setOperationId(stepList.get(stepList.size() - 1).getOperationId()); + wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); + + if (tmpPosition != -1) { + dragEntities.add(tmpPosition, wrapper); + tmpPosition = -1; + } else { + dragEntities.add(wrapper); + } + + adapter.notifyDataSetChanged(); + + // 录制模式需要记录下 break; default: break; @@ -197,7 +267,10 @@ private void initData() { final List stepTags = new ArrayList<>(stepList.size() + 2); // 每一步添加一个实体 - stepTags.add("新步骤"); + stepTags.add(getString(R.string.step_edit__new_step)); + stepTags.add(getString(R.string.step_edit__select_mode)); + stepTags.add(getString(R.string.step_edit__paste)); + stepTags.add(getString(R.string.step_edit__record_step)); for (OperationStep step: stepList) { CaseStepAdapter.MyDataWrapper entity = new CaseStepAdapter.MyDataWrapper(clone(step), currentIdx.getAndIncrement()); dragEntities.add(entity); @@ -218,12 +291,30 @@ public View getView(FlowLayout parent, int position, String o) { ImageView icon = (ImageView) tag.findViewById(R.id.case_step_edit_tag_icon); if (position == 0) { - title.setText("新步骤"); - icon.setImageResource(R.drawable.case_step_add); + if (!selectMode) { + title.setText(R.string.step_edit__new_step); + icon.setImageResource(R.drawable.case_step_add); + } else { + title.setText(R.string.step_edit__copy); + icon.setImageResource(R.drawable.case_step_copy); + } + } else if (position == 1) { + if (selectMode) { + title.setText(R.string.step_edit__exit_select); + } else { + title.setText(R.string.step_edit__select_mode); + } + icon.setImageResource(R.drawable.case_step_select); + } else if (position == 2) { + title.setText(o); + icon.setImageResource(R.drawable.case_step_paste); + } else if (position == 3) { + title.setText(o); + icon.setImageResource(R.drawable.recording); } else { // 加载下 - OperationStep step = stepList.get(position - 1); + OperationStep step = stepList.get(position - 4); PerformActionEnum actionEnum = step.getOperationMethod().getActionEnum(); // 设置资源 @@ -237,54 +328,194 @@ public View getView(FlowLayout parent, int position, String o) { // 用例adapter adapter = new CaseStepAdapter(getActivity(), dragEntities); + adapter.setCurrentMode(selectMode); // 设置菜单相关样式 - int dp64 = ContextUtil.dip2px(getActivity(), 64); + int dp64 = getResources().getDimensionPixelSize(R.dimen.control_dp64); + int textSize13 = ContextUtil.px2sp(getActivity(), getResources().getDimensionPixelSize(R.dimen.textsize_14)); int colorWhile; int colorIf; + int colorDelete; + int colorExtra; if (Build.VERSION.SDK_INT >= 23) { colorWhile = getActivity().getColor(R.color.colorStatusBlue); - colorIf = getActivity().getColor(R.color.colorStatusRed); + colorIf = getActivity().getColor(R.color.colorStatusYellow); + colorDelete = getActivity().getColor(R.color.colorStatusRed); + colorExtra = getActivity().getColor(R.color.colorStatusGay); } else { colorWhile = getResources().getColor(R.color.colorStatusBlue); - colorIf = getResources().getColor(R.color.colorStatusRed); + colorIf = getResources().getColor(R.color.colorStatusYellow); + colorDelete = getResources().getColor(R.color.colorStatusRed); + colorExtra = getResources().getColor(R.color.colorStatusGay); } // 转换模式 Menu menu = new Menu(true, 0); - menu.addItem(new MenuItem.Builder().setText("转换为IF").setTextColor(Color.WHITE).setWidth(dp64) + menu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__remove)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setBackground(new ColorDrawable(colorDelete)).build()); + menu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__convert_if)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) .setDirection(MenuItem.DIRECTION_RIGHT) .setBackground(new ColorDrawable(colorIf)).build()); - menu.addItem(new MenuItem.Builder().setText("转换为WHILE").setTextColor(Color.WHITE) + menu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__convert_while)).setTextColor(Color.WHITE) .setWidth(dp64) + .setTextSize(textSize13) .setDirection(MenuItem.DIRECTION_RIGHT) .setBackground(new ColorDrawable(colorWhile)).build()); // 空项 Menu controlMenu = new Menu(false, 1); + controlMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__remove)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setBackground(new ColorDrawable(colorDelete)).build()); + controlMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__restore_step)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setBackground(new ColorDrawable(colorIf)).build()); - dragList.setMenu(menu, controlMenu); + // 空项 + Menu controlSubMenu = new Menu(false, 2); + controlSubMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__remove)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setBackground(new ColorDrawable(colorDelete)).build()); + + // 转换模式 + Menu clickMenu = new Menu(true, 3); + clickMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__remove)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setBackground(new ColorDrawable(colorDelete)).build()); + clickMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__convert_if)).setTextColor(Color.WHITE).setWidth(dp64) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setTextSize(textSize13) + .setBackground(new ColorDrawable(colorIf)).build()); + clickMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__convert_while)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setTextSize(textSize13) + .setBackground(new ColorDrawable(colorWhile)).build()); + clickMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__convert_click_if_exist)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setBackground(new ColorDrawable(colorExtra)).build()); + + + // 转换模式 + Menu clickIfMenu = new Menu(true, 4); + clickIfMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__remove)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setBackground(new ColorDrawable(colorDelete)).build()); + clickIfMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__convert_if)).setTextColor(Color.WHITE).setWidth(dp64) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setTextSize(textSize13) + .setBackground(new ColorDrawable(colorIf)).build()); + clickIfMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__convert_while)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setTextSize(textSize13) + .setBackground(new ColorDrawable(colorWhile)).build()); + clickIfMenu.addItem(new MenuItem.Builder().setText(getString(R.string.step_edit__convert_click)).setTextColor(Color.WHITE) + .setWidth(dp64) + .setTextSize(textSize13) + .setDirection(MenuItem.DIRECTION_RIGHT) + .setBackground(new ColorDrawable(colorExtra)).build()); + + dragList.setMenu(menu, controlMenu, controlSubMenu, clickMenu, clickIfMenu); dragList.setDividerHeight(0); dragList.setAdapter(adapter); + adapter.setListener(new CaseStepAdapter.OnStepListener() { + @Override + public void insertAfter(int position) { + showSelectModeAction(position + 1); + } + + @Override + public void scroll(final int px) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + dragList.smoothScrollBy(px, 100); + } + }, 100); + } + }); dragList.setOnMenuItemClickListener(new SlideAndDragListView.OnMenuItemClickListener() { @Override - public int onMenuItemClick(View v, int itemPosition, int buttonPosition, int direction) { + public int onMenuItemClick(View v, final int itemPosition, int buttonPosition, int direction) { if (direction == MenuItem.DIRECTION_RIGHT) { + if (buttonPosition == 0) { + LauncherApplication.getInstance().showDialog(getActivity(), "是否删除该步骤?", "确定", new Runnable() { + @Override + public void run() { + dragEntities.remove(itemPosition); + adapter.notifyDataSetChanged(); + } + }, "取消", null); + return Menu.ITEM_NOTHING; + } else if (buttonPosition == 3) { + CaseStepAdapter.MyDataWrapper wrapper = dragEntities.get(itemPosition); + OperationMethod method = wrapper.currentStep.getOperationMethod(); + PerformActionEnum origin = method.getActionEnum(); + if (origin == PerformActionEnum.CLICK) { + method.setActionEnum(PerformActionEnum.CLICK_IF_EXISTS); + adapter.notifyDataSetChanged(); + return Menu.ITEM_SCROLL_BACK; + } else if (origin == PerformActionEnum.CLICK_IF_EXISTS) { + method.setActionEnum(PerformActionEnum.CLICK); + adapter.notifyDataSetChanged(); + return Menu.ITEM_SCROLL_BACK; + } else { + CaseStepEditFragment.this.toastShort("不支持转化步骤: " + origin.getDesc()); + return Menu.ITEM_SCROLL_BACK; + } + + + } + // 全部操作均支持转化 CaseStepAdapter.MyDataWrapper wrapper = dragEntities.get(itemPosition); OperationMethod method = wrapper.currentStep.getOperationMethod(); PerformActionEnum origin = method.getActionEnum(); - if (buttonPosition == 0) { - method.setActionEnum(PerformActionEnum.IF); - wrapper.scopeTo = wrapper.idx + 1; - } else if (buttonPosition == 1) { + if (buttonPosition == 1) { + if (origin == PerformActionEnum.IF || origin == PerformActionEnum.WHILE) { + String checkVal = method.getParam(LogicUtil.CHECK_PARAM); + if (!StringUtil.startWith(checkVal, LogicUtil.ASSERT_ACTION_PREFIX)) { + LauncherApplication.getInstance().showToast("无法转化为原始方法"); + return Menu.ITEM_SCROLL_BACK; + } + String originCode = checkVal.substring(LogicUtil.ASSERT_ACTION_PREFIX.length()); + PerformActionEnum action = PerformActionEnum.getActionEnumByCode(originCode); + method.setActionEnum(action); + wrapper.scopeTo = -1; + method.removeParam(LogicUtil.CHECK_PARAM); + method.removeParam(SCOPE); + } else { + method.setActionEnum(PerformActionEnum.IF); + wrapper.scopeTo = wrapper.idx + 1; + // 设置assert条件 + method.putParam(LogicUtil.CHECK_PARAM, LogicUtil.ASSERT_ACTION_PREFIX + origin.getCode()); + } + } else if (buttonPosition == 2) { method.setActionEnum(PerformActionEnum.WHILE); wrapper.scopeTo = wrapper.idx + 1; + // 设置assert条件 + method.putParam(LogicUtil.CHECK_PARAM, LogicUtil.ASSERT_ACTION_PREFIX + origin.getCode()); } - // 设置assert条件 - method.putParam(LogicUtil.CHECK_PARAM, LogicUtil.ASSERT_ACTION_PREFIX + origin.getCode()); adapter.notifyDataSetChanged(); return Menu.ITEM_SCROLL_BACK; @@ -302,14 +533,58 @@ public void onItemClick(AdapterView parent, View view, int position, long id) }); } + /** + * 切换选择模式 + */ + private void switchSelectMode() { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + selectMode = !selectMode; + tagGroup.getAdapter().notifyDataChanged(); + adapter.setCurrentMode(selectMode); + } + }); + } + @Override public boolean onTagClick(View view, int position, FlowLayout parent) { if (position == 0) { - showAddFunctionView(); + if (!selectMode) { + showSelectModeAction(dragEntities.size()); + } else { + List wrappers = adapter.getAndClearSelectOperationSteps(); + if (wrappers.size() == 0) { + return true; + } + List steps = new ArrayList<>(wrappers.size() + 1); + for (CaseStepAdapter.MyDataWrapper wrapper: wrappers) { + steps.add(wrapper.currentStep); + } + + CaseStepHolder.storePasteContent(steps); + switchSelectMode(); + } return true; + } else if (position == 1) { + switchSelectMode(); + return true; + } else if (position == 2) { + List pasteSteps = CaseStepHolder.getPasteContent(); + if (pasteSteps != null && pasteSteps.size() > 0) { + for (OperationStep step: pasteSteps) { + CaseStepAdapter.MyDataWrapper wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); + dragEntities.add(wrapper); + } + + adapter.notifyDataSetChanged(); + } + return true; + } else if (position == 3) { + return addRecordCases(-1); } - OperationStep step = stepList.get(position - 1); + OperationStep step = stepList.get(position - 4); CaseStepAdapter.MyDataWrapper entity = new CaseStepAdapter.MyDataWrapper(clone(step), currentIdx.getAndIncrement()); // 如果是if和while,需要设置为0 @@ -345,6 +620,61 @@ private void saveScopeInfo() { } } + private boolean addRecordCases(final int position) { + final CaseEditActivity activity = (CaseEditActivity) getActivity(); + activity.wrapRecordCase(); + + final RecordCaseInfo caseInfo = activity.getRecordCase(); + if (caseInfo == null) { + return false; + } + // 检查权限 + PermissionUtil.requestPermissions(Arrays.asList("adb", Settings.ACTION_ACCESSIBILITY_SETTINGS), activity, new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(boolean result, String reason) { + if (result) { + showProgressDialog(getString(R.string.step_edit__now_loading)); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + boolean prepareResult = PrepareUtil.doPrepareWork(caseInfo.getTargetAppPackage(), new PrepareUtil.PrepareStatus() { + @Override + public void currentStatus(int progress, int total, String message, boolean status) { + updateProgressDialog(progress, total, message); + } + }); + + + if (prepareResult) { + final CaseAppendOperationProcessor processor = new CaseAppendOperationProcessor(caseInfo); + dismissProgressDialog(); + if (position > -1) { + processor.setInsertPosition(position); + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + LauncherApplication.service(OperationStepService.class).registerStepProcessor(processor); + CaseRecordManager manager = LauncherApplication.service(CaseRecordManager.class); + manager.setRecordCase(caseInfo); + + AppUtil.startApp(caseInfo.getTargetAppPackage()); + activity.finish(); + } + }); + } else { + dismissProgressDialog(); + toastShort(getString(R.string.step_edit__prepare_env_fail)); + } + } + }); + } + } + }); + + return true; + } + @Override public void onCaseSave() { // 同步下scope信息 @@ -384,17 +714,16 @@ private void showEditDialog(final CaseStepAdapter.MyDataWrapper wrapper) { final TabLayout tab = (TabLayout) v.findViewById(R.id.dialog_case_step_edit_tab); if (clone.getOperationNode() != null) { TabLayout.Tab tabItem = tab.newTab(); - tabItem.setText("Node"); + tabItem.setText(R.string.step_edit__node_info); tab.addTab(tabItem); tabItem.select(); nodeAdapter = new CaseStepNodeAdapter(clone.getOperationNode()); } else { nodeAdapter = null; } - TabLayout.Tab tabItem = tab.newTab(); - tabItem.setText("Method"); - tab.addTab(tabItem); + tabItem.setText(R.string.step_edit__method_info); + tab.addTab(tabItem, 0); // 配置后续列表 final List laterList; @@ -420,11 +749,10 @@ private void showEditDialog(final CaseStepAdapter.MyDataWrapper wrapper) { final CaseStepMethodAdapter paramAdapter = new CaseStepMethodAdapter(laterList, clone.getOperationMethod()); - r.setAdapter(nodeAdapter != null? nodeAdapter: paramAdapter); tab.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { - if (StringUtil.equals(tab.getText(), "Node")) { + if (StringUtil.equals(tab.getText(), getString(R.string.step_edit__node_info))) { r.setAdapter(nodeAdapter); } else { r.setAdapter(paramAdapter); @@ -441,6 +769,25 @@ public void onTabReselected(TabLayout.Tab tab) { } }); + + // 配置选中的tab + if (paramAdapter.getItemCount() > 0) { + tabItem.select(); + r.setAdapter(paramAdapter); + } else { + if (nodeAdapter == null) { + tabItem.select(); + r.setAdapter(paramAdapter); + } else { + TabLayout.Tab nodeTab = tab.getTabAt(1); + if (nodeTab != null) { + nodeTab.select(); + } + + r.setAdapter(nodeAdapter); + } + } + DialogInterface.OnClickListener dialogClick = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -459,8 +806,8 @@ public void onClick(DialogInterface dialog, int which) { }; final AlertDialog dialog = new AlertDialog.Builder(getActivity()) - .setView(v).setPositiveButton("确定", dialogClick) - .setNegativeButton("取消", dialogClick) + .setView(v).setPositiveButton(R.string.constant__confirm, dialogClick) + .setNegativeButton(R.string.constant__cancel, dialogClick) .setTitle(clone.getOperationMethod().getActionEnum().getDesc()).create(); dialog.show(); @@ -481,92 +828,263 @@ private OperationStep clone(OperationStep origin) { } /** - * 更新用例 - * @param mRecordCase + * 展示选择添加步骤模式 */ - private void doUpdateCase(final RecordCaseInfo mRecordCase) { - BackgroundExecutor.execute(new Runnable() { - @Override - public void run() { - mRecordCase.setGmtModify(System.currentTimeMillis()); - GreenDaoManager.getInstance().getRecordCaseInfoDao().save(mRecordCase); - toastShort("更新成功"); - InjectorService.g().pushMessage(NewRecordActivity.NEED_REFRESH_LOCAL_CASES_LIST); - } - }); + private void showSelectModeAction(final int position) { + final String[] actions = new String[]{getString(R.string.case_step_edit__node_action), + getString(R.string.case_step_edit__global_action), + getString(R.string.case_step_edit__record_add_action)}; + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.AppDialogTheme) + .setTitle(R.string.case_step_edit__select_add_action) + .setSingleChoiceItems(actions, -1, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + LogUtil.i(TAG, "Click " + which); + + if (dialog != null) { + dialog.dismiss(); + } + + if (which == 0) { + showCreateNodeView(position); + } else if (which == 1){ + showAddFunctionView(null, position); + } else if (which == 2) { + addRecordCases(position); + } + } + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + + AlertDialog dialog = builder.create(); + dialog.setCanceledOnTouchOutside(false); + dialog.setCancelable(false); + dialog.show(); } /** - * 显示添加操作界面 + * 展示创建控件界面 */ - private void showAddFunctionView() { - FunctionSelectUtil.showFunctionView(getActivity(), null, GLOBAL_KEYS, GLOBAL_ICONS, - GLOBAL_ACTION_MAP, null, null, null, - new FunctionSelectUtil.FunctionListener() { + private void showCreateNodeView(final int position) { + // 渲染创建控件的View + View createNodeView = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_create_node, null); + final EditText className = (EditText) createNodeView.findViewById(R.id.create_node_classname); + final EditText text = (EditText) createNodeView.findViewById(R.id.create_node_text); + final EditText resId = (EditText) createNodeView.findViewById(R.id.create_node_res_id); + final EditText xpath = (EditText) createNodeView.findViewById(R.id.create_node_xpath); + + final AlertDialog dialog = new AlertDialog.Builder(getActivity()) + .setView(createNodeView).setPositiveButton(R.string.case_step_edit__select_action, new DialogInterface.OnClickListener() { @Override - public void onProcessFunction(OperationMethod method, AbstractNodeTree node) { - PerformActionEnum action = method.getActionEnum(); - if (action == PerformActionEnum.JUMP_TO_PAGE) { + public void onClick(DialogInterface dialog, int which) { + OperationNode node = new OperationNode(); + String classNameText = className.getText().toString(); + if (StringUtil.isEmpty(classNameText)) { + classNameText = "*"; + } + node.setClassName(classNameText); - if (StringUtil.equals(method.getParam("scan"), "1")) { - // 注册下Service - InjectorService injectorService = LauncherApplication.getInstance().findServiceByName(InjectorService.class.getName()); - injectorService.register(CaseStepEditFragment.this); + String textText = text.getText().toString(); + if (StringUtil.isEmpty(textText)) { + textText = null; + } + node.setText(textText); + node.setDescription(textText); + String resIdText = resId.getText().toString(); + if (StringUtil.isEmpty(resIdText)) { + resIdText = null; + } + node.setResourceId(resIdText); - Intent intent = new Intent(getActivity(), QRScanActivity.class); + String xpathText = xpath.getText().toString(); + if (StringUtil.isEmpty(xpathText)) { + xpathText = null; + } + node.setXpath(xpathText); - if (action == PerformActionEnum.JUMP_TO_PAGE) { - intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_SCHEME); - } - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - return; - } + if (dialog != null) { + dialog.dismiss(); } - OperationStep step = new OperationStep(); - step.setOperationMethod(method); + // 默认使用辅助功能控件 + node.setNodeType(AccessibilityNodeTree.class.getSimpleName()); + showAddFunctionView(node, position); + } + }) + .setNegativeButton(R.string.constant__cancel, null) + .setTitle(R.string.case_step_edit__set_node_info).create(); + dialog.show(); + + // 选择第一个 + dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + } - // 从空添加 - if (stepList.size() > 0) { + /** + * 显示添加操作界面 + */ + private void showAddFunctionView(final OperationNode node, final int position) { + if (node != null) { + AbstractNodeTree tmpNode = new FakeLocatingNode(node); + FunctionSelectUtil.showFunctionView(getActivity(), tmpNode, NODE_KEYS, NODE_ICONS, + NODE_ACTION_MAP, null, null, null, + new FunctionSelectUtil.FunctionListener() { + @Override + public void onProcessFunction(final OperationMethod method, AbstractNodeTree fake) { + PerformActionEnum action = method.getActionEnum(); + OperationStep step = new OperationStep(); + step.setOperationMethod(method); OperationStep lastStep = stepList.get(stepList.size() - 1); step.setOperationId(lastStep.getOperationId()); step.setOperationIndex(lastStep.getOperationIndex() + 1); - } else { - step.setOperationId("1"); - step.setOperationIndex(1); + step.setOperationNode(node); + + CaseStepAdapter.MyDataWrapper wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); + dragEntities.add(position, wrapper); + adapter.notifyDataSetChanged(); } - CaseStepAdapter.MyDataWrapper wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); + @Override + public void onCancel() { - // if和while设置下scope - if (method.getActionEnum() == PerformActionEnum.IF || method.getActionEnum() == PerformActionEnum.WHILE) { - wrapper.scopeTo = wrapper.idx; } + }); + } else { + FunctionSelectUtil.showFunctionView(getActivity(), null, GLOBAL_KEYS, GLOBAL_ICONS, + GLOBAL_ACTION_MAP, null, null, null, + new FunctionSelectUtil.FunctionListener() { + @Override + public void onProcessFunction(OperationMethod method, AbstractNodeTree node) { + PerformActionEnum action = method.getActionEnum(); + if (action == PerformActionEnum.JUMP_TO_PAGE + || action == PerformActionEnum.GENERATE_QR_CODE + || action == PerformActionEnum.LOAD_PARAM) { + + if (StringUtil.equals(method.getParam("scan"), "1")) { + // 注册下Service + InjectorService injectorService = LauncherApplication.getInstance().findServiceByName(InjectorService.class.getName()); + injectorService.register(CaseStepEditFragment.this); + + + Intent intent = new Intent(getActivity(), QRScanActivity.class); + if (action == PerformActionEnum.JUMP_TO_PAGE) { + intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_SCHEME); + } else if (action == PerformActionEnum.GENERATE_QR_CODE) { + intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_QR_CODE); + } else if (action == PerformActionEnum.LOAD_PARAM) { + intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_PARAM); + } + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (position != dragEntities.size()) { + tmpPosition = position; + } else { + tmpPosition = -1; + } + startActivity(intent); + return; + } + } - dragEntities.add(wrapper); + OperationStep step = new OperationStep(); + step.setOperationMethod(method); + + // 从空添加 + if (stepList.size() > 0) { + OperationStep lastStep = stepList.get(stepList.size() - 1); + step.setOperationId(lastStep.getOperationId()); + step.setOperationIndex(lastStep.getOperationIndex() + 1); + } else { + step.setOperationId("1"); + step.setOperationIndex(1); + } - adapter.notifyDataSetChanged(); - } + CaseStepAdapter.MyDataWrapper wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); + + // if和while设置下scope,不要在最后一位 + if (method.getActionEnum() == PerformActionEnum.IF || method.getActionEnum() == PerformActionEnum.WHILE) { + if (dragEntities.size() > 0) { + CaseStepAdapter.MyDataWrapper last = dragEntities.get(dragEntities.size() - 1); + wrapper.scopeTo = last.idx; + dragEntities.add(Math.min(dragEntities.size() - 1, position), wrapper); + } else { + wrapper.scopeTo = wrapper.idx; + dragEntities.add(position, wrapper); + } + } else { + dragEntities.add(position, wrapper); + } - @Override - public void onCancel() { - } - }); + adapter.notifyDataSetChanged(); + } + + @Override + public void onCancel() { + + } + }); + } } - protected static final List GLOBAL_KEYS = new ArrayList<>(); + protected static final List GLOBAL_KEYS = new ArrayList<>(); protected static final List GLOBAL_ICONS = new ArrayList<>(); - protected static final Map> GLOBAL_ACTION_MAP = new HashMap<>(); + protected static final Map> GLOBAL_ACTION_MAP = new HashMap<>(); + + protected static final List NODE_KEYS = new ArrayList<>(); + + protected static final List NODE_ICONS = new ArrayList<>(); + + protected static final Map> NODE_ACTION_MAP = new HashMap<>(); + // 初始化二级菜单 static { + // 节点操作 + NODE_KEYS.add(R.string.function_group__click); + NODE_ICONS.add(R.drawable.dialog_action_drawable_quick_click_2); + List clickActions = new ArrayList<>(); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK)); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.LONG_CLICK)); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_IF_EXISTS)); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_QUICK)); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_AND_INPUT)); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.MULTI_CLICK)); + NODE_ACTION_MAP.put(R.string.function_group__click, clickActions); + + NODE_KEYS.add(R.string.function_group__input); + NODE_ICONS.add(R.drawable.dialog_action_drawable_input); + List inputActions = new ArrayList<>(); + inputActions.add(convertPerformActionToSubMenu(PerformActionEnum.INPUT)); + inputActions.add(convertPerformActionToSubMenu(PerformActionEnum.INPUT_SEARCH)); + NODE_ACTION_MAP.put(R.string.function_group__input, inputActions); + + NODE_KEYS.add(R.string.function_group__scroll); + NODE_ICONS.add(R.drawable.dialog_action_drawable_scroll); + List scrollActions = new ArrayList<>(); + scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCROLL_TO_BOTTOM)); + scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCROLL_TO_TOP)); + scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCROLL_TO_LEFT)); + scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCROLL_TO_RIGHT)); + scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GESTURE)); + NODE_ACTION_MAP.put(R.string.function_group__scroll, scrollActions); + + NODE_KEYS.add(R.string.function_group__assert); + NODE_ICONS.add(R.drawable.dialog_action_drawable_assert); + List assertActions = new ArrayList<>(); + assertActions.add(convertPerformActionToSubMenu(PerformActionEnum.ASSERT)); + assertActions.add(convertPerformActionToSubMenu(PerformActionEnum.SLEEP_UNTIL)); + assertActions.add(convertPerformActionToSubMenu(PerformActionEnum.LET_NODE)); + assertActions.add(convertPerformActionToSubMenu(PerformActionEnum.CHECK_NODE)); + NODE_ACTION_MAP.put(R.string.function_group__assert, assertActions); + // 全局操作 - GLOBAL_KEYS.add("device"); + GLOBAL_KEYS.add(R.string.function_group__device); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_device_operation); List gDeviceActions = new ArrayList<>(); gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.BACK)); @@ -577,35 +1095,44 @@ public void onCancel() { gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.EXECUTE_SHELL)); gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.NOTIFICATION)); gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.RECENT_TASK)); - GLOBAL_ACTION_MAP.put("device", gDeviceActions); + GLOBAL_ACTION_MAP.put(R.string.function_group__device, gDeviceActions); - GLOBAL_KEYS.add("app"); + GLOBAL_KEYS.add(R.string.function_group__app); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_app_operation); List gAppActions = new ArrayList<>(); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.GOTO_INDEX)); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.CHANGE_MODE)); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.JUMP_TO_PAGE)); + gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.GENERATE_QR_CODE)); + gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.GENERATE_BAR_CODE)); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.KILL_PROCESS)); - GLOBAL_ACTION_MAP.put("app", gAppActions); + GLOBAL_ACTION_MAP.put(R.string.function_group__app, gAppActions); - GLOBAL_KEYS.add("scroll"); + GLOBAL_KEYS.add(R.string.function_group__scroll); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_scroll); List gScrollActions = new ArrayList<>(); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_BOTTOM)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_TOP)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_LEFT)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_RIGHT)); - GLOBAL_ACTION_MAP.put("scroll", gScrollActions); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.KEYBOARD_INPUT)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.INPUT_GLOBAL)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_PINCH_OUT)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_PINCH_IN)); + GLOBAL_ACTION_MAP.put(R.string.function_group__scroll, gScrollActions); // 循环逻辑控制 - GLOBAL_KEYS.add("logic"); + GLOBAL_KEYS.add(R.string.function_group__logic); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_logic); List gLoopActions = new ArrayList<>(); gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.IF)); + gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.LET)); + gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.CHECK)); gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.WHILE)); gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.CONTINUE)); gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.BREAK)); - GLOBAL_ACTION_MAP.put("logic", gLoopActions); + gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.LOAD_PARAM)); + GLOBAL_ACTION_MAP.put(R.string.function_group__logic, gLoopActions); } /** @@ -617,4 +1144,31 @@ private static TwoLevelSelectLayout.SubMenuItem convertPerformActionToSubMenu(Pe return new TwoLevelSelectLayout.SubMenuItem(actionEnum.getDesc(), actionEnum.getCode(), actionEnum.getIcon()); } + + + private static class FakeLocatingNode extends AbstractNodeTree { + private FakeLocatingNode(OperationNode node) { + setClassName(node.getClassName()); + setText(node.getText()); + setDescription(node.getDescription()); + setXpath(node.getXpath()); + setResourceId(node.getResourceId()); + setVisible(true); + } + + @Override + public boolean canDoAction(PerformActionEnum action) { + return false; + } + + @Override + public StringBuilder printTrace(StringBuilder builder) { + return null; + } + + @Override + public boolean isSelfUsableForLocating() { + return true; + } + } } diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/LocalReplayResultListFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/LocalReplayResultListFragment.java new file mode 100644 index 0000000..e9acd79 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/fragment/LocalReplayResultListFragment.java @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.fragment; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.TextView; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONReader; +import com.alibaba.fastjson.TypeReference; +import com.alipay.hulu.R; +import com.alipay.hulu.activity.CaseReplayResultActivity; +import com.alipay.hulu.adapter.LocalTaskResultListAdapter; +import com.alipay.hulu.bean.CaseStepHolder; +import com.alipay.hulu.bean.ReplayResultBean; +import com.alipay.hulu.bean.ReplayStepInfoBean; +import com.alipay.hulu.bean.ScreenshotBean; +import com.alipay.hulu.common.bean.DeviceInfo; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.FileUtils; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; + +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import androidx.annotation.Nullable; +import androidx.collection.ArrayMap; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +public class LocalReplayResultListFragment extends BaseFragment { + private static final String TAG = "LocalResultListFrag"; + private static final String KEY_ARG_FRAGMENT_TYPE = "KEY_ARG_FRAGMENT_TYPE"; + + public static final int KEY_LIST_TYPE_LOCAL = 0; + + private int type; + private ListView mListView; + private SwipeRefreshLayout refreshLayout; + private View mEmptyView; + private TextView mEmptyTextView; + private LocalTaskResultListAdapter mAdapter; + private List localResultList; + + public static LocalReplayResultListFragment newInstance(int type) { + LocalReplayResultListFragment fragment = new LocalReplayResultListFragment(); + Bundle args = new Bundle(); + args.putInt(KEY_ARG_FRAGMENT_TYPE, type); + fragment.setArguments(args); + return fragment; + } + + public static int[] getAvailableTypes() { + return new int[] {KEY_LIST_TYPE_LOCAL}; + } + + public static String getTypeName(int type) { + if (type == KEY_LIST_TYPE_LOCAL) { + return StringUtil.getString(R.string.replay_list__local); + } + + return null; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle bundle = getArguments(); + if (bundle == null) { + return; + } + type = bundle.getInt(KEY_ARG_FRAGMENT_TYPE); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_replay_list, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initEmptyView(view); + initListView(view); + + // 读取用例 + if (type == KEY_LIST_TYPE_LOCAL) { + getReplayResultFromFile(null); + } + } + + private void getReplayResultFromFile(final Runnable r) { + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + File root = FileUtils.getSubDir("replay"); + File[] subDirs = root.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory() && new File(pathname, "info.json").exists(); + } + }); + + if (subDirs == null || subDirs.length == 0) { + localResultList = null; + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (r != null) { + r.run(); + } + mListView.setVisibility(View.GONE); + mEmptyView.setVisibility(View.VISIBLE); + } + }); + return; + } + + List resultBeans = new ArrayList<>(subDirs.length + 1); + for (File folder: subDirs) { + File info = new File(folder, "info.json"); + try { + ReplayResultBean result = JSON.parseObject(new FileInputStream(info), ReplayResultBean.class); + result.setBaseDir(folder); + File deviceInfo = new File(folder, "device.json"); + if (deviceInfo.exists()) { + result.setDeviceInfo((DeviceInfo) JSON.parseObject(new FileInputStream(deviceInfo), DeviceInfo.class)); + } + resultBeans.add(result); + } catch (IOException e) { + LogUtil.w(TAG, "Fail to load result info in folder " + folder, e); + } + } + + // 按创建时间排序 + Collections.sort(resultBeans, new Comparator() { + @Override + public int compare(ReplayResultBean o1, ReplayResultBean o2) { + return o2.getStartTime().compareTo(o1.getStartTime()); + } + }); + + localResultList = resultBeans; + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (r != null) { + r.run(); + } + if (localResultList != null && localResultList.size() > 0) { + mAdapter.setData(localResultList); + mListView.setVisibility(View.VISIBLE); + mEmptyView.setVisibility(View.GONE); + } else { + mListView.setVisibility(View.GONE); + mEmptyView.setVisibility(View.VISIBLE); + } + } + }); + } + }); + } + + private void initListView(View view) { + refreshLayout = view.findViewById(R.id.replay_swipe_refresh); + refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + Runnable r = new Runnable() { + @Override + public void run() { + refreshLayout.setRefreshing(false); + } + }; + + // 读取用例 + if (type == KEY_LIST_TYPE_LOCAL) { + getReplayResultFromFile(r); + } + } + }); + + mListView = view.findViewById(R.id.replay_list); + mAdapter = new LocalTaskResultListAdapter(getContext()); + + mListView.setAdapter(mAdapter); + + // 默认点击编辑 + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + showReplayResult(position); + } + }); + + // 长按删除 + mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + @Override + public boolean onItemLongClick(AdapterView parent, View view, final int position, long id) { + new AlertDialog.Builder(getActivity()) + .setMessage(R.string.replay_result__delete_item) + .setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + } + }) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + deleteResult(position); + } + }) + .show(); + return true; + } + }); + } + + /** + * 删除回放结果 + * @param position + */ + private void deleteResult(int position) { + + if (type == KEY_LIST_TYPE_LOCAL) { + ReplayResultBean result = localResultList.get(position); + File baseDir = result.getBaseDir(); + if (baseDir != null && baseDir.exists()) { + try { + FileUtils.deleteDirectory(baseDir); + getReplayResultFromFile(null); + } catch (IOException e) { + LogUtil.e(TAG, "Fail delete folder " + baseDir, e); + toastShort("删除回放文件失败,请尝试手动删除"); + } + } + } + } + + private void initEmptyView(View view) { + mEmptyView = view.findViewById(R.id.empty_view_container); + mEmptyTextView = view.findViewById(R.id.empty_text); + mEmptyTextView.setText(R.string.replay_result__no_history); + } + + /** + * 展示回放结果 + * @param position + */ + private void showReplayResult(int position) { + ReplayResultBean resultBean = localResultList.get(position); + File baseDir = resultBean.getBaseDir(); + try { + Map actionLogs = new JSONReader(new FileReader(new File(baseDir, "actions.json"))).readObject(new TypeReference>() {}); + resultBean.setActionLogs(actionLogs); + } catch (IOException e) { + LogUtil.e(TAG, "Fail to find ", e); + } + + File targetFile = new File(baseDir, "running.log"); + resultBean.setLogFile(targetFile.getPath()); + + File steps = new File(baseDir, "steps.json"); + try { + List operations = new JSONReader(new FileReader(steps)).readObject(new TypeReference>() {}); + resultBean.setCurrentOperationLog(operations); + } catch (IOException e) { + LogUtil.e(TAG, "Fail to find ", e); + } + + List screenshotBeans = resultBean.getScreenshots(); + if (screenshotBeans != null) { + ArrayMap screenshots = new ArrayMap<>(); + for (ScreenshotBean screenshot: screenshotBeans) { + if (screenshot == null || StringUtil.isEmpty(screenshot.getFile()) || StringUtil.isEmpty(screenshot.getName())) { + continue; + } + screenshots.put(screenshot.getName(), new File(baseDir, screenshot.getFile()).getPath()); + } + resultBean.setScreenshotFiles(screenshots); + } + + Intent intent = new Intent(getActivity(), CaseReplayResultActivity.class); + + // 通过Holder中转 + int storeId = CaseStepHolder.storeResult(resultBean); + intent.putExtra("data", storeId); + startActivity(intent); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayListFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayListFragment.java index 8751082..5a84d28 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayListFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayListFragment.java @@ -19,8 +19,9 @@ import android.content.Intent; import android.os.Bundle; import android.provider.Settings; -import android.support.annotation.Nullable; -import android.support.v7.app.AlertDialog; +import androidx.annotation.Nullable; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import androidx.appcompat.app.AlertDialog; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; @@ -36,12 +37,13 @@ import com.alibaba.fastjson.JSON; import com.alipay.hulu.R; import com.alipay.hulu.activity.CaseEditActivity; +import com.alipay.hulu.activity.CaseParamEditActivity; import com.alipay.hulu.activity.MyApplication; import com.alipay.hulu.activity.NewRecordActivity; import com.alipay.hulu.adapter.ReplayListAdapter; import com.alipay.hulu.bean.CaseStepHolder; -import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.SubscribeParamEnum; import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.tools.BackgroundExecutor; @@ -51,9 +53,6 @@ import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.common.utils.StringUtil; -import com.alipay.hulu.replay.OperationStepProvider; -import com.alipay.hulu.replay.RepeatStepProvider; -import com.alipay.hulu.service.CaseReplayManager; import com.alipay.hulu.shared.io.bean.GeneralOperationLogBean; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; import com.alipay.hulu.shared.io.db.GreenDaoManager; @@ -61,6 +60,7 @@ import com.alipay.hulu.shared.io.util.OperationStepUtil; import com.alipay.hulu.shared.node.action.PerformActionEnum; import com.alipay.hulu.shared.node.utils.AppUtil; +import com.alipay.hulu.util.CaseReplayUtil; import com.alipay.hulu.util.DialogUtils; import java.io.BufferedWriter; @@ -85,6 +85,8 @@ public class ReplayListFragment extends BaseFragment { private View mEmptyView; private TextView mEmptyTextView; private ReplayListAdapter mAdapter; + private SwipeRefreshLayout refreshLayout; + private String app; public static ReplayListFragment newInstance(int type) { return new ReplayListFragment(); @@ -95,7 +97,7 @@ public static int[] getAvailableTypes() { } public static String getTypeName(int type) { - return "本地"; + return StringUtil.getString(R.string.replay_list__local); } @Override @@ -104,6 +106,11 @@ public void onCreate(@Nullable Bundle savedInstanceState) { InjectorService.g().register(this); } + @Subscriber(@Param(SubscribeParamEnum.APP)) + public void setApp(String app) { + this.app = app; + } + @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -116,7 +123,7 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { initEmptyView(view); initListView(view); - getReplayRecordsFromDB(); + getReplayRecordsFromDB(null); } /** @@ -124,10 +131,10 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { */ @Subscriber(value = @Param(value = NewRecordActivity.NEED_REFRESH_LOCAL_CASES_LIST, sticky = false)) public void reloadLocalCases() { - getReplayRecordsFromDB(); + getReplayRecordsFromDB(null); } - private void getReplayRecordsFromDB() { + private void getReplayRecordsFromDB(final Runnable r) { BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -137,6 +144,9 @@ public void run() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { + if (r != null) { + r.run(); + } if (mCases != null && mCases.size() > 0) { mAdapter.updateData(mCases); mListView.setVisibility(View.VISIBLE); @@ -152,21 +162,29 @@ public void run() { } private void initListView(View view) { + refreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.replay_swipe_refresh); + refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + Runnable r = new Runnable() { + @Override + public void run() { + refreshLayout.setRefreshing(false); + } + }; + + // 读取用例 + getReplayRecordsFromDB(r); + } + }); + mListView = (ListView) view.findViewById(R.id.replay_list); mAdapter = new ReplayListAdapter(getContext()); mListView.setAdapter(mAdapter); - // 设置编辑按键监听器 - mAdapter.setOnEditClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - editCase(position); - } - }); - - // 默认点击播放 - mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + // 设置播放按键监听器 + mAdapter.setOnPlayClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { final RecordCaseInfo caseInfo = (RecordCaseInfo) mAdapter.getItem(position); @@ -178,11 +196,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) @Override public void onPermissionResult(final boolean result, String reason) { if (result) { - OperationStepProvider stepProvider = new OperationStepProvider(caseInfo); - MyApplication.getInstance().updateAppAndNameTemp(caseInfo.getTargetAppPackage(), caseInfo.getTargetAppLabel()); - CaseReplayManager manager = LauncherApplication.getInstance().findServiceByName(CaseReplayManager.class.getName()); - manager.start(stepProvider, MyApplication.SINGLE_REPLAY_LISTENER); - + CaseReplayUtil.startReplay(caseInfo); startTargetApp(caseInfo.getTargetAppPackage()); } } @@ -190,6 +204,14 @@ public void onPermissionResult(final boolean result, String reason) { } }); + // 默认点击编辑 + mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + editCase(position); + } + }); + mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView parent, View view, final int position, long id) { @@ -208,6 +230,9 @@ public void onExecute(DialogInterface dialog, PerformActionEnum action) { case PLAY_MULTI_TIMES: repeatPrepare(position); break; + case GEN_MULTI_PARAM: + genMultiParams(position); + break; } } @@ -244,6 +269,19 @@ private void editCase(int position) { startActivity(intent); } + private void genMultiParams(final int position) { + RecordCaseInfo caseInfo = (RecordCaseInfo) mAdapter.getItem(position); + if (caseInfo == null) { + return; + } + caseInfo = caseInfo.clone(); + + Intent intent = new Intent(getActivity(), CaseParamEditActivity.class); + int caseId = CaseStepHolder.storeCase(caseInfo); + intent.putExtra(CaseParamEditActivity.RECORD_CASE_EXTRA, caseId); + startActivity(intent); + } + /** * 删除用例 * @param position @@ -343,7 +381,7 @@ protected void repeatPrepare(final int position) { textPattern = Pattern.compile("\\d{1,3}"); final AlertDialog dialog = new AlertDialog.Builder(getActivity(), R.style.AppDialogTheme) - .setTitle("请输入回放次数") + .setTitle(R.string.replay__set_replay_count) .setView(v) .setPositiveButton(R.string.constant__start_execution, new DialogInterface.OnClickListener() { @Override @@ -409,11 +447,7 @@ private void playMultiTimeCase(final int position, final int count, final boolea @Override public void onPermissionResult(final boolean result, String reason) { if (result) { - RepeatStepProvider stepProvider = new RepeatStepProvider(caseInfo, count, prepare); - MyApplication.getInstance().updateAppAndNameTemp(caseInfo.getTargetAppPackage(), caseInfo.getTargetAppLabel()); - CaseReplayManager manager = LauncherApplication.getInstance().findServiceByName(CaseReplayManager.class.getName()); - manager.start(stepProvider, MyApplication.MULTI_REPLAY_LISTENER); - + CaseReplayUtil.startReplayMultiTimes(caseInfo, count, prepare); startTargetApp(caseInfo.getTargetAppPackage()); } } diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayLogFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayLogFragment.java index 7f2b0df..8f6a63a 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayLogFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayLogFragment.java @@ -16,8 +16,6 @@ package com.alipay.hulu.fragment; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -33,6 +31,9 @@ import java.io.FileReader; import java.io.IOException; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + public class ReplayLogFragment extends Fragment { public static final String LOG_FILE_PATH_TAG = "logFilePath"; @@ -101,9 +102,11 @@ public void run() { String line; int readCount = 0; - while ((line = reader.readLine()) != null && readCount < 301) { + final boolean readEmpty = (line = reader.readLine()) == null; + while (line != null && readCount < 301) { sb.append(line).append('\n'); readCount++; + line = reader.readLine(); } final boolean tooLong = readCount > 300; @@ -115,6 +118,9 @@ public void run() { if (tooLong) { tooLoneText.setVisibility(View.VISIBLE); tooLoneText.setText(String.format(getString(R.string.to_long_template), adbPath)); + } else if (readEmpty) { + tooLoneText.setVisibility(View.VISIBLE); + tooLoneText.setText(String.format(getString(R.string.log__read_fail_template), adbPath)); } } }); diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayMainResultFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayMainResultFragment.java index 50b9a95..434dff3 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayMainResultFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayMainResultFragment.java @@ -17,10 +17,10 @@ import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; @@ -29,6 +29,7 @@ import com.alipay.hulu.R; import com.alipay.hulu.bean.ReplayResultBean; +import com.alipay.hulu.common.bean.DeviceInfo; import com.alipay.hulu.common.utils.DeviceInfoUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; @@ -77,12 +78,16 @@ private void wrapDisplayData() { contents = new ArrayList<>(); - contents.add(new Pair<>("设备信息", DeviceInfoUtil.generateDeviceInfo().toString())); + DeviceInfo deviceInfo = resultBean.getDeviceInfo(); + if (deviceInfo == null) { + deviceInfo = DeviceInfoUtil.generateDeviceInfo(); + } + contents.add(new Pair<>(getString(R.string.ui__device_info), deviceInfo.toString())); List operations = resultBean.getCurrentOperationLog(); // 拼接流程信息 - StringBuilder operationString = new StringBuilder("总步骤数:").append(operations.size()).append("\n\n"); + StringBuilder operationString = new StringBuilder(getString(R.string.ui__total_steps)).append(operations.size()).append("\n\n"); for (int i = 0; i < operations.size(); i++) { OperationStep currentOperation = operations.get(i); operationString.append(i + 1).append(" ").append(currentOperation.getOperationMethod().getActionEnum().getDesc()); @@ -105,13 +110,13 @@ private void wrapDisplayData() { operationString.append("\n"); } - contents.add(new Pair<>("用例流程", operationString.toString())); + contents.add(new Pair<>(getString(R.string.ui__case_steps), operationString.toString())); // 如果回访失败,显示故障相关信息 if (!StringUtil.isEmpty(resultBean.getExceptionMessage())) { - contents.add(new Pair<>("故障步骤", Integer.toString(resultBean.getExceptionStep() + 1))); + contents.add(new Pair<>(getString(R.string.ui__error_step), Integer.toString(resultBean.getExceptionStep() + 1))); - contents.add(new Pair<>("故障原因", resultBean.getExceptionMessage())); + contents.add(new Pair<>(getString(R.string.ui__error_reason), resultBean.getExceptionMessage())); } } diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayScreenShotFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayScreenShotFragment.java index 1c78714..8891c35 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayScreenShotFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayScreenShotFragment.java @@ -16,10 +16,10 @@ package com.alipay.hulu.fragment; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; @@ -31,11 +31,14 @@ import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.util.DialogUtils; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -51,7 +54,7 @@ public class ReplayScreenShotFragment extends Fragment { private RecyclerView recyclerView; private ReplayResultBean resultBean; - List> screenshots; + List> screenshots; public static ReplayScreenShotFragment newInstance(ReplayResultBean data) { ReplayScreenShotFragment fragment = new ReplayScreenShotFragment(); @@ -79,20 +82,40 @@ public void onCreate(@Nullable Bundle savedInstanceState) { Map screenshotFiles = resultBean.getScreenshotFiles(); if (screenshotFiles != null) { - List> screenshots = new ArrayList<>(); + List> screenshots = new ArrayList<>(); File screenshotDir = FileUtils.getSubDir("screenshots"); // 组装各项 for (Map.Entry entry : screenshotFiles.entrySet()) { - File targetFile = new File(screenshotDir, entry.getValue() + ".png"); - Pair target; - if (targetFile.exists()) { - target = new Pair<>(entry.getKey(), targetFile); + String path = entry.getValue(); + if (StringUtil.startWith(path, "https://") || StringUtil.startWith(path, "http://")) { + try { + URL url = new URL(path); + screenshots.add(new Pair(entry.getKey(), url)); + } catch (MalformedURLException e) { + LogUtil.w(TAG, "Fail to load url " + path, e); + } + } else if (StringUtil.startWith(path, "/")) { + File targetFile = new File(path); + Pair target; + if (targetFile.exists()) { + target = new Pair(entry.getKey(), targetFile); + } else { + target = new Pair<>(entry.getKey(), null); + } + + screenshots.add(target); } else { - target = new Pair<>(entry.getKey(), null); + File targetFile = new File(screenshotDir, entry.getValue() + ".png"); + Pair target; + if (targetFile.exists()) { + target = new Pair(entry.getKey(), targetFile); + } else { + target = new Pair<>(entry.getKey(), null); + } + + screenshots.add(target); } - - screenshots.add(target); } this.screenshots = screenshots; @@ -132,8 +155,13 @@ public void onBindViewHolder(ScreenshotHolder holder, int position) { } // 加载内容 - Pair screenshot = screenshots.get(position); - holder.loadData(screenshot.first, screenshot.second); + Pair screenshot = screenshots.get(position); + Object target = screenshot.second; + if (target instanceof File) { + holder.loadData(screenshot.first, (File) target); + } else if (target instanceof URL) { + holder.loadData(screenshot.first, (URL) target); + } } @Override @@ -151,6 +179,7 @@ private static class ScreenshotHolder extends RecyclerView.ViewHolder implements private TextView locationText; private ImageView img; private File previousFile; + private URL previousOnlineImg; public ScreenshotHolder(View itemView) { super(itemView); @@ -180,10 +209,22 @@ private void loadData(String name, File target) { } } + private void loadData(String name, URL target) { + nameText.setText(name); + locationText.setText(target.toString()); + Glide.with(img.getContext()) + .load(target) + .apply(RequestOptions.fitCenterTransform()) + .into(img); + previousOnlineImg = target; + } + @Override public void onClick(View v) { if (previousFile != null) { DialogUtils.showImageDialog(img.getContext(), previousFile); + } else if (previousOnlineImg != null) { + DialogUtils.showImageDialog(img.getContext(), previousOnlineImg); } } diff --git a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayStepFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayStepFragment.java index 65daf75..63906a1 100644 --- a/src/app/src/main/java/com/alipay/hulu/fragment/ReplayStepFragment.java +++ b/src/app/src/main/java/com/alipay/hulu/fragment/ReplayStepFragment.java @@ -16,13 +16,12 @@ package com.alipay.hulu.fragment; import android.content.DialogInterface; -import android.graphics.Bitmap; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.appcompat.app.AlertDialog; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; @@ -33,14 +32,14 @@ import com.alipay.hulu.R; import com.alipay.hulu.actions.ImageCompareActionProvider; +import com.alipay.hulu.bean.CaseStepStatus; import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.bean.ReplayStepInfoBean; import com.alipay.hulu.common.utils.GlideApp; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; -import com.alipay.hulu.shared.node.action.OperationExecutor; import com.alipay.hulu.shared.node.action.OperationMethod; -import com.alipay.hulu.shared.node.tree.export.OperationStepProvider; +import com.alipay.hulu.shared.node.tree.export.OperationStepExporter; import com.alipay.hulu.shared.node.tree.OperationNode; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; import com.alipay.hulu.shared.node.utils.BitmapUtil; @@ -127,12 +126,12 @@ public ResultItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @Override public void onBindViewHolder(ResultItemViewHolder holder, int position) { Pair data = contents.get(position); - String action = "已完成"; + CaseStepStatus action = CaseStepStatus.FINISH; if (!StringUtil.isEmpty(resultBean.getExceptionMessage())) { if (resultBean.getExceptionStep() == position) { - action = "失败"; + action = CaseStepStatus.FAIL; } else if (resultBean.getExceptionStep() < position) { - action = "尚未执行"; + action = CaseStepStatus.UNENFORCED; } } holder.bindData(data.first, data.second == null? new ReplayStepInfoBean(): data.second, action); @@ -193,7 +192,7 @@ private static class ResultItemViewHolder extends RecyclerView.ViewHolder implem mFindCapture.setOnClickListener(this); } - void bindData(OperationStep operation, ReplayStepInfoBean replay, String status) { + void bindData(OperationStep operation, ReplayStepInfoBean replay, CaseStepStatus status) { mActionName.setText(operation.getOperationMethod().getActionEnum().getDesc()); StringBuilder sb; @@ -235,27 +234,27 @@ void bindData(OperationStep operation, ReplayStepInfoBean replay, String status) } // 配置状态 - if (StringUtil.equals(status, "已完成")) { + if (status == CaseStepStatus.FINISH) { mStatus.setTextColor(0xff65c0ba); - } else if (StringUtil.equals(status, "失败")) { + } else if (status == CaseStepStatus.FAIL) { mStatus.setTextColor(0xfff76262); } else { mStatus.setTextColor(mStatus.getResources().getColor(R.color.secondaryText)); } - mStatus.setText(status); + mStatus.setText(status.getName()); boolean captureFlag = false; try { // 获取base64信息 findBytes = BitmapUtil.decodeBase64(findNode == null? null: - findNode.getExtraValue(OperationStepProvider.CAPTURE_IMAGE_BASE64)); + findNode.getExtraValue(OperationStepExporter.CAPTURE_IMAGE_BASE64)); targetBytes = null; if (method != null) { if (method.containsParam(ImageCompareActionProvider.KEY_TARGET_IMAGE)) { targetBytes = BitmapUtil.decodeBase64(method.getParam(ImageCompareActionProvider.KEY_TARGET_IMAGE)); - } else if (node != null && node.containsExtra(OperationStepProvider.CAPTURE_IMAGE_BASE64)) { - targetBytes = BitmapUtil.decodeBase64(node.getExtraValue(OperationStepProvider.CAPTURE_IMAGE_BASE64)); + } else if (node != null && node.containsExtra(OperationStepExporter.CAPTURE_IMAGE_BASE64)) { + targetBytes = BitmapUtil.decodeBase64(node.getExtraValue(OperationStepExporter.CAPTURE_IMAGE_BASE64)); } } @@ -277,12 +276,15 @@ void bindData(OperationStep operation, ReplayStepInfoBean replay, String status) LogUtil.e("ReplayStepFrag", "配置控件截图信息失败", e); } + if (node != null) { + mNodeRow.setVisibility(View.VISIBLE); + } else { + mNodeRow.setVisibility(View.GONE); + } // 如果有设置截图信息 if (captureFlag) { - mNodeRow.setVisibility(View.VISIBLE); mCaptureRow.setVisibility(View.VISIBLE); } else { - mNodeRow.setVisibility(View.GONE); mCaptureRow.setVisibility(View.GONE); } } @@ -312,8 +314,8 @@ public void onClick(View v) { private void showContentDialog(OperationNode node) { AlertDialog dialog = new AlertDialog.Builder(mTargetNode.getContext()) .setView(wrapView(node)) - .setTitle("节点结构") - .setPositiveButton("确定", new DialogInterface.OnClickListener() { + .setTitle(R.string.replay__node_struct) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); diff --git a/src/app/src/main/java/com/alipay/hulu/prepare/StartAppPreparer.java b/src/app/src/main/java/com/alipay/hulu/prepare/StartAppPreparer.java new file mode 100644 index 0000000..7eab6d4 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/prepare/StartAppPreparer.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.prepare; + +import com.alipay.hulu.R; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.service.SPService; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.node.OperationService; +import com.alipay.hulu.shared.node.action.OperationContext; +import com.alipay.hulu.shared.node.action.OperationExecutor; +import com.alipay.hulu.shared.node.action.OperationMethod; +import com.alipay.hulu.shared.node.action.PerformActionEnum; +import com.alipay.hulu.shared.node.utils.AppUtil; +import com.alipay.hulu.shared.node.utils.PrepareUtil; +import com.alipay.hulu.shared.node.utils.prepare.PrepareWorker; + +import java.util.concurrent.CountDownLatch; + +/** + * Created by qiaoruikai on 2019/10/9 9:34 PM. + */ +@PrepareWorker.PrepareTool(priority = 0) +public class StartAppPreparer implements PrepareWorker { + private static final String TAG = "StartAppPreparer"; + public static final String KEY_PREPARED_APP_ALERT = "K_preparedAppAlert"; + + @Override + public boolean doPrepareWork(String targetApp, PrepareUtil.PrepareStatus status) { + if (!SPService.getBoolean(SPService.KEY_RESTART_APP_ON_PLAY, true)) { + return true; + } + if (status != null) { + status.currentStatus(100, 100, StringUtil.getString(R.string.prepare__restart_app), true); + } + + AppUtil.startApp(targetApp); + + OperationService service = LauncherApplication.service(OperationService.class); + String clearAppData = (String) service.getRuntimeParam(StopAppPreparer.KEY_CLEARED_APP_DATA); + String preparedAppAlert = (String) service.getRuntimeParam(KEY_PREPARED_APP_ALERT); + if ("true".equals(clearAppData) && !("true".equals(preparedAppAlert))) { + if (status != null) { + status.currentStatus(100, 100, StringUtil.getString(R.string.prepare__handle_start_alert), true); + } + + // 处理清理数据后弹出的权限弹窗 + final CountDownLatch latch = new CountDownLatch(1); + OperationMethod method = new OperationMethod(PerformActionEnum.HANDLE_ALERT); + service.doSomeAction(method, null, new OperationContext.BaseOperationListener() { + @Override + public void notifyOperationFinish() { + latch.countDown(); + } + }); + + try { + latch.await(); + } catch (InterruptedException e) { + LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); + } + + service.putRuntimeParam(KEY_PREPARED_APP_ALERT, "true"); + } + return true; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/prepare/StopAppPreparer.java b/src/app/src/main/java/com/alipay/hulu/prepare/StopAppPreparer.java new file mode 100644 index 0000000..f68a23e --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/prepare/StopAppPreparer.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.prepare; + +import com.alipay.hulu.R; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.service.SPService; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.node.OperationService; +import com.alipay.hulu.shared.node.action.OperationMethod; +import com.alipay.hulu.shared.node.action.PerformActionEnum; +import com.alipay.hulu.shared.node.utils.AppUtil; +import com.alipay.hulu.shared.node.utils.PrepareUtil; +import com.alipay.hulu.shared.node.utils.prepare.PrepareWorker; + +/** + * 清理数据准备器 + * Created by qiaoruikai on 2019-10-09 12:15. + */ +@PrepareWorker.PrepareTool(priority = Integer.MAX_VALUE) +public class StopAppPreparer implements PrepareWorker { + public static final String KEY_CLEAR_APP_DATA = "K_clearAppData"; + public static final String KEY_CLEARED_APP_DATA = "K_clearedAppData"; + @Override + public boolean doPrepareWork(String targetApp, PrepareUtil.PrepareStatus status) { + // 拉起应用 + if (!SPService.getBoolean(SPService.KEY_RESTART_APP_ON_PLAY, true)) { + AppUtil.launchTargetApp(targetApp); + return true; + } + + OperationService service = LauncherApplication.service(OperationService.class); + String clearAppData = (String) service.getRuntimeParam(KEY_CLEAR_APP_DATA); + String clearedAppData = (String) service.getRuntimeParam(KEY_CLEARED_APP_DATA); + if ("true".equals(clearAppData) && !("true".equals(clearedAppData))) { + if (status != null) { + status.currentStatus(100, 100, StringUtil.getString(R.string.prepare__clear_app_data), true); + } + + AppUtil.clearAppData(targetApp); + + service.putRuntimeParam(KEY_CLEARED_APP_DATA, "true"); + } + + AppUtil.forceStopApp(targetApp); + AppUtil.forceStopApp(targetApp); + + return true; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/replay/AbstractStepProvider.java b/src/app/src/main/java/com/alipay/hulu/replay/AbstractStepProvider.java index b387f40..1856901 100644 --- a/src/app/src/main/java/com/alipay/hulu/replay/AbstractStepProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/replay/AbstractStepProvider.java @@ -15,19 +15,27 @@ */ package com.alipay.hulu.replay; +import android.app.ActivityManager; import android.content.Context; import android.content.DialogInterface; -import android.support.v7.app.AlertDialog; +import androidx.appcompat.app.AlertDialog; + +import android.os.Build; import android.view.View; -import android.view.WindowManager; import com.alipay.hulu.R; import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.bean.ReplayStepInfoBean; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.service.CaseReplayManager; +import com.alipay.hulu.shared.node.action.PerformActionEnum; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; +import com.alipay.hulu.util.DialogUtils; +import java.io.IOException; import java.util.Arrays; import java.util.Date; import java.util.List; @@ -72,7 +80,7 @@ public boolean canStart() { * @param reason 故障原因 * @return 是否是故障 */ - public abstract boolean reportErrorStep(OperationStep step, String reason); + public abstract boolean reportErrorStep(OperationStep step, String reason, List callStack); /** * 获取回放结果 @@ -94,12 +102,62 @@ public List genReplayResult() { public abstract void onStepInfo(ReplayStepInfoBean bean); public void onFloatClick(Context context, final CaseReplayManager manager) { - showFunctionView(context, "是否终止回放", new Runnable() { + DialogUtils.showFunctionView(context, Arrays.asList(PerformActionEnum.NORMAL_EXIT, PerformActionEnum.FORCE_STOP), new DialogUtils.FunctionViewCallback() { + + @Override + public void onExecute(DialogInterface dialog, PerformActionEnum action) { + if (action == PerformActionEnum.NORMAL_EXIT) { + manager.stopRunning(); + } else if (action == PerformActionEnum.FORCE_STOP) { + // 移除所有Task + ActivityManager am = (ActivityManager) LauncherApplication.getInstance() + .getSystemService(Context.ACTIVITY_SERVICE); + if (am != null && Build.VERSION.SDK_INT >= 21) { + try { + List tasks = am.getAppTasks(); + for (ActivityManager.AppTask task: tasks) { + task.finishAndRemoveTask(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + int pid = android.os.Process.myPid(); + String command = "kill -9 "+ pid; + try { + Runtime.getRuntime().exec(command); + } catch (IOException e) { + LogUtil.e(TAG, "强制关闭进程失败"); + } + // adb强杀 + try { + String cmd = "am force-stop " + LauncherApplication.getInstance().getPackageName(); + CmdTools.execCmd(cmd + " && " + cmd); + } catch (Throwable e) { + LogUtil.w(TAG, "force-stop fail??", e); + } + } + }, 200); + + // System exit + System.exit(0); + } + dialog.dismiss(); + } + + @Override + public void onCancel(DialogInterface dialog) { + dialog.dismiss(); + } + @Override - public void run() { - manager.stopRunning(); + public void onDismiss(DialogInterface dialog) { + } - }, null); + }); } /** @@ -110,45 +168,4 @@ public void run() { public View provideView(Context context) { return null; } - - /** - * 展示操作dialog - * @param message 消息 - * @param confirmAction 确定动作 - * @param cancelAction 取消动作 - */ - protected void showFunctionView(Context context, String message, final Runnable confirmAction, final Runnable cancelAction) { - try { - AlertDialog dialog = new AlertDialog.Builder(context, R.style.SimpleDialogTheme) - .setMessage(message) - .setPositiveButton("确定", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (confirmAction != null) { - confirmAction.run(); - } - dialog.dismiss(); - } - }) - .setNegativeButton("取消", new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (cancelAction != null) { - cancelAction.run(); - } - dialog.dismiss(); - } - }).setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - dialog.dismiss(); - } - }).create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } catch (Exception e) { - LogUtil.e(TAG, e.getMessage()); - } - } } diff --git a/src/app/src/main/java/com/alipay/hulu/replay/BatchStepProvider.java b/src/app/src/main/java/com/alipay/hulu/replay/BatchStepProvider.java index 97f4c05..5ff855c 100644 --- a/src/app/src/main/java/com/alipay/hulu/replay/BatchStepProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/replay/BatchStepProvider.java @@ -112,8 +112,8 @@ public boolean hasNext() { } @Override - public boolean reportErrorStep(OperationStep step, String reason) { - boolean errorResult = currentStepProvider.reportErrorStep(step, reason); + public boolean reportErrorStep(OperationStep step, String reason, List stack) { + boolean errorResult = currentStepProvider.reportErrorStep(step, reason, stack); // 如果是关键性错误 if (errorResult) { diff --git a/src/app/src/main/java/com/alipay/hulu/replay/MultiParamStepProvider.java b/src/app/src/main/java/com/alipay/hulu/replay/MultiParamStepProvider.java new file mode 100644 index 0000000..3487f72 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/replay/MultiParamStepProvider.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.replay; + +import androidx.annotation.NonNull; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.bean.AdvanceCaseSetting; +import com.alipay.hulu.bean.CaseRunningParam; +import com.alipay.hulu.bean.ReplayResultBean; +import com.alipay.hulu.bean.ReplayStepInfoBean; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.node.OperationService; +import com.alipay.hulu.shared.node.action.OperationMethod; +import com.alipay.hulu.shared.node.action.PerformActionEnum; +import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by qiaoruikai on 2019-08-20 19:26. + */ +public class MultiParamStepProvider extends AbstractStepProvider { + private static final String TAG = "RepeatStepProvider"; + + private OperationService operationService; + + private RecordCaseInfo recordCase; + private OperationStep prepareStep; + + private int currentIdx; + private List> repeatParams = new ArrayList<>(); + + OperationStepProvider currentStepProvider; + + List resultBeans; + + @Override + public void prepare() { + loadStep(); + } + + public MultiParamStepProvider(@NonNull RecordCaseInfo recordCase) { + this.recordCase = recordCase; + currentIdx = 0; + operationService = LauncherApplication.service(OperationService.class); + + parseParams(); + resultBeans = new ArrayList<>(repeatParams.size() + 1); + } + + private void parseParams() { + AdvanceCaseSetting setting = JSON.parseObject(recordCase.getAdvanceSettings(), AdvanceCaseSetting.class); + CaseRunningParam runningParam = setting.getRunningParam(); + if (runningParam == null) { + repeatParams.add(Collections.EMPTY_MAP); + return; + } + + if (runningParam.getMode() == CaseRunningParam.ParamMode.UNION) { + List paramUnion = runningParam.getParamList(); + for (JSONObject param: paramUnion) { + Map realParams = new HashMap<>(param.size() + 1); + for (String key: param.keySet()) { + realParams.put(key, param.getString(key)); + } + + repeatParams.add(realParams); + } + } else { + Map> paramSet = new HashMap<>(); + List paramUnion = runningParam.getParamList(); + for (JSONObject param: paramUnion) { + for (String key: param.keySet()) { + paramSet.put(key, Arrays.asList(StringUtil.split(param.getString(key), ","))); + } + } + + List keys = new ArrayList<>(paramSet.keySet()); + if (keys.size() == 0) { + return; + } + + List> stackParam = new ArrayList<>(); + String initKey = keys.get(0); + for (String param: paramSet.get(initKey)) { + HashMap realParams = new HashMap<>(keys.size() + 1); + realParams.put(initKey, param); + stackParam.add(realParams); + } + + // 全连接网络 + for (int i = 1; i < keys.size(); i++) { + List> newStackParam = new ArrayList<>(); + String key = keys.get(i); + for (Map realParam: stackParam) { + for (String param: paramSet.get(key)) { + Map newLevelParam = new HashMap<>(realParam); + newLevelParam.put(key, param); + newStackParam.add(newLevelParam); + } + } + stackParam = newStackParam; + } + + repeatParams.addAll(stackParam); + } + } + + private void loadStep() { + if (currentIdx <= repeatParams.size() - 1) { + currentStepProvider = new OperationStepProvider(recordCase); + currentStepProvider.putParams(repeatParams.get(currentIdx)); + currentIdx++; + + currentStepProvider.prepare(); + + prepareStep = new OperationStep(); + prepareStep.setOperationMethod(new OperationMethod(PerformActionEnum.GOTO_INDEX)); + } else { + currentStepProvider = null; + } + } + + @Override + public OperationStep provideStep() { + if (prepareStep != null) { + OperationStep step = prepareStep; + prepareStep = null; + return step; + } + return currentStepProvider == null? null: currentStepProvider.provideStep(); + } + + @Override + public boolean hasNext() { + if (currentStepProvider != null && !currentStepProvider.hasNext()) { + resultBeans.addAll(currentStepProvider.genReplayResult()); + loadStep(); + } + + return currentStepProvider != null && currentStepProvider.hasNext(); + } + + @Override + public boolean reportErrorStep(OperationStep step, String reason, List stack) { + boolean errorResult = currentStepProvider.reportErrorStep(step, reason, stack); + + // 如果是关键性错误 + if (errorResult) { + // 记录下之前的问题 + resultBeans.addAll(currentStepProvider.genReplayResult()); + + // 加载下一步 + loadStep(); + } + + return false; + } + + @Override + public void onStepInfo(ReplayStepInfoBean bean) { + currentStepProvider.onStepInfo(bean); + } + + @Override + public List genReplayResult() { + if (currentStepProvider != null) { + resultBeans.addAll(currentStepProvider.genReplayResult()); + currentStepProvider = null; + } + + return resultBeans; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/replay/OperationStepProvider.java b/src/app/src/main/java/com/alipay/hulu/replay/OperationStepProvider.java index a1ef4eb..2199869 100644 --- a/src/app/src/main/java/com/alipay/hulu/replay/OperationStepProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/replay/OperationStepProvider.java @@ -15,23 +15,30 @@ */ package com.alipay.hulu.replay; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.text.TextUtils; import com.alibaba.fastjson.JSON; +import com.alipay.hulu.R; import com.alipay.hulu.activity.MyApplication; import com.alipay.hulu.bean.AdvanceCaseSetting; +import com.alipay.hulu.bean.CaseParamBean; import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.bean.ReplayStepInfoBean; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.tools.CmdTools; +import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.io.bean.GeneralOperationLogBean; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; import com.alipay.hulu.shared.io.util.OperationStepUtil; import com.alipay.hulu.shared.node.OperationService; +import com.alipay.hulu.shared.node.action.OperationContext; import com.alipay.hulu.shared.node.action.OperationExecutor; import com.alipay.hulu.shared.node.action.OperationMethod; import com.alipay.hulu.shared.node.action.PerformActionEnum; +import com.alipay.hulu.shared.node.tree.AbstractNodeTree; import com.alipay.hulu.shared.node.tree.OperationNode; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; import com.alipay.hulu.shared.node.utils.LogicUtil; @@ -46,7 +53,10 @@ import java.util.Locale; import java.util.Map; import java.util.Stack; -import java.util.Vector; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + import static com.alipay.hulu.shared.node.utils.LogicUtil.CHECK_PARAM; import static com.alipay.hulu.shared.node.utils.LogicUtil.SCOPE; @@ -55,6 +65,8 @@ */ public class OperationStepProvider extends AbstractStepProvider { private static final String TAG = "OpStepProvider"; + private static final Pattern FILED_CALL_PATTERN = Pattern.compile("\\$\\{[^}\\s]+\\.?[^}\\s]*\\}"); + protected OperationService operationService; private List stepList = new ArrayList<>(); @@ -63,6 +75,8 @@ public class OperationStepProvider extends AbstractStepProvider { protected Map screenshotFiles; protected String targetApp; + protected String targetAppPkg; + protected String targetAppVersionName; protected RecordCaseInfo caseInfo; @@ -82,33 +96,145 @@ public class OperationStepProvider extends AbstractStepProvider { */ protected boolean waitForCheck; + /** + * 是否初始化环境 + */ + protected boolean initEnvironment; + /** * check结果 */ protected int checkIdx = -1; private String errorReason; + protected String errorStepId; protected int currentIdx; + protected Map initParams = new HashMap<>(); + + /** + * 参数映射处理 + */ + private OperationMethod.ParamProcessor paramReplacer = new OperationMethod.ParamProcessor() { + @Override + public String filterParam(String key, String value, PerformActionEnum action) { + return getMappedContent(value, operationService); + } + }; + + /** + * 将当期运行时变量映射到字符串中 + * + * @param origin + * @param service + * @return + */ + public static String getMappedContent(String origin, final OperationService service) { + if (service == null) { + return origin; + } + + return StringUtil.patternReplace(origin, FILED_CALL_PATTERN, new StringUtil.PatternReplace() { + @Override + public String replacePattern(String origin) { + String content = origin.substring(2, origin.length() - 1); + // 有子内容调用 + if (content.contains(".")) { + String[] group = content.split("\\.", 2); + + if (group.length != 2) { + return origin; + } + + // 获取当前变量 + Object obj = service.getRuntimeParam(group[0]); + if (obj == null) { + return origin; + } + + LogUtil.d(TAG, "Map key word %s to value %s", group[0], obj); + + // 特殊判断 + // 节点字段,自行操作 + if (obj instanceof AbstractNodeTree) { + String replace = StringUtil.toString(((AbstractNodeTree) obj).getField(group[1])); + if (replace == null) { + return origin; + } else { + return replace; + } + } else { + // 目前只支持length方法 + if (StringUtil.equals(group[1], "length")) { + return Integer.toString(StringUtil.toString(obj).length()); + } else { + return origin; + } + } + } else { + String target = StringUtil.toString(service.getRuntimeParam(content)); + if (target == null) { + return origin; + } else { + return target; + } + } + } + }); + } + public OperationStepProvider(RecordCaseInfo caseInfo) { + this(caseInfo, true); + } + + public OperationStepProvider(RecordCaseInfo caseInfo, boolean initParams) { this.caseInfo = caseInfo; loadOperation(caseInfo.getOperationLog()); currentStepInfo = new HashMap<>(); screenshotFiles = new LinkedHashMap<>(); + initEnvironment = initParams; currentIdx = 0; // 加载OperationService operationService = LauncherApplication.getInstance().findServiceByName(OperationService.class.getName()); } + /** + * 配置初始化参数 + * @param params + */ + public void putParams(Map params) { + if (params == null || params.size() == 0) { + return; + } + + initParams.putAll(params); + } + @Override public void prepare() { super.prepare(); - CmdTools.startAppLog(); - operationService.initParams(); - MyApplication.getInstance().updateAppAndNameTemp(caseInfo.getTargetAppPackage(), caseInfo.getTargetAppLabel()); + if (initEnvironment) { + CmdTools.startAppLog(); + operationService.initParams(); + MyApplication.getInstance().updateAppAndNameTemp(caseInfo.getTargetAppPackage(), caseInfo.getTargetAppLabel()); + } + + operationService.putAllRuntimeParamAtTop(initParams); + targetApp = caseInfo.getTargetAppLabel(); + targetAppPkg = caseInfo.getTargetAppPackage(); + targetAppVersionName = null; + + try { + PackageInfo info = LauncherApplication.getInstance().getPackageManager().getPackageInfo(targetAppPkg, 0); + if (info != null) { + targetAppVersionName = info.versionName; + } + } catch (PackageManager.NameNotFoundException e) { + LogUtil.w(TAG, "Fail to load pkg " + targetApp, e); + } } public void loadOperation(String content) { @@ -141,15 +267,24 @@ protected void addSetupStepsIfNeeded() { , stepList.get(0).getOperationId()); stepList.add(0, changeModeBean); } + + // 参数信息 + if (setting.getParams() != null && setting.getParams().size() > 0) { + Map params = new HashMap<>(setting.getParams().size() + 1); + for (CaseParamBean caseParam: setting.getParams()) { + params.put(caseParam.getParamName(), caseParam.getParamDefaultValue()); + } + + // 设置参数 + initParams.putAll(params); + } } } } @Override public OperationStep provideStep() { - /** - * loop循环 - */ + // loop循环 LoopParam param; while ((param = loopParams.peek()) != null) { @@ -183,7 +318,9 @@ public OperationStep provideStep() { // screen shot改下名 if (method.getActionEnum() == PerformActionEnum.SCREENSHOT) { - String screenShotName = method.getParam(OperationExecutor.INPUT_TEXT_KEY); + String screenShotName = OperationExecutor.getMappedContent( + method.getParam(OperationExecutor.INPUT_TEXT_KEY), operationService); + Date now = new Date(); SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSS_", Locale.CHINA); String newFileName = format.format(now) + screenShotName; @@ -211,9 +348,12 @@ public OperationStep provideStep() { return checkStep; } else if (method.getActionEnum() == PerformActionEnum.WHILE) { String status = method.getParam(CHECK_PARAM); + status = OperationExecutor.getMappedContent(status, operationService); + + String scopeContent = OperationExecutor.getMappedContent(method.getParam(SCOPE), operationService); if (StringUtil.startWith(status, LogicUtil.LOOP_PREFIX)) { LoopParam newParam = new LoopParam(currentIdx, - currentIdx - 1 + Integer.parseInt(method.getParam(SCOPE)), + currentIdx - 1 + Integer.parseInt(scopeContent), Integer.parseInt(status.substring(6)) - 2); // 循环次数小于1,直接跳出去 @@ -245,7 +385,7 @@ public OperationStep provideStep() { // 循环配置 LoopParam newParam = new LoopParam(currentIdx - 1, - currentIdx - 1 + Integer.parseInt(method.getParam(SCOPE)), 0); + currentIdx - 1 + Integer.parseInt(scopeContent), 0); loopParams.push(newParam); return checkStep; @@ -298,7 +438,9 @@ public boolean hasNext() { } @Override - public boolean reportErrorStep(OperationStep step, String reason) { + public boolean reportErrorStep(OperationStep step, String reason, List stack) { + stack.add("Error at step " + currentIdx + " " + step.getOperationMethod().getActionEnum().getDesc()); + // 未查找到,也接收 if (waitForCheck && currentIdx == checkIdx + 1 && (StringUtil.equals(reason, "执行失败") || StringUtil.equals(reason, "节点未查找到"))) { @@ -313,7 +455,7 @@ public boolean reportErrorStep(OperationStep step, String reason) { ifIdx = -1; - // Loop信息校验 + // Loop信息校验 } else if ((l = loopParams.peek()) != null && currentIdx == l.loopPos + 1) { OperationStep whileStep = stepList.get(l.loopPos); @@ -323,7 +465,9 @@ public boolean reportErrorStep(OperationStep step, String reason) { loopParams.pop(); } else { - this.errorReason = reason; + this.errorReason = reason + "\n" + StringUtil.join("\n", stack); + errorStepId = step.getStepId(); + takeScreenshot(); return true; } @@ -331,10 +475,43 @@ public boolean reportErrorStep(OperationStep step, String reason) { return false; } - this.errorReason = reason; + this.errorReason = reason + "\n" + StringUtil.join("\n", stack); + errorStepId = step.getStepId(); + takeScreenshot(); return true; } + /** + * 进行截图 + */ + protected void takeScreenshot() { + // 执行失败,进行截图 + OperationMethod method = new OperationMethod(PerformActionEnum.SCREENSHOT); + + // 生成文件名 + Date now = new Date(); + SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSS_", Locale.CHINA); + String newFileName = format.format(now) + StringUtil.getString(R.string.step_provider__error_step, currentIdx); + method.putParam(OperationExecutor.INPUT_TEXT_KEY, newFileName); + screenshotFiles.put(StringUtil.getString(R.string.step_provider__error_step, currentIdx), newFileName); + + final CountDownLatch latch = new CountDownLatch(1); + // 执行截图操作 + operationService.doSomeAction(method, null, new OperationContext.BaseOperationListener() { + @Override + public void notifyOperationFinish() { + latch.countDown(); + } + }); + + // 等5s截图保存 + try { + latch.await(5000, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); + } + } + @Override public void onStepInfo(ReplayStepInfoBean bean) { currentStepInfo.put(currentIdx - 1, bean); @@ -350,20 +527,24 @@ public List genReplayResult() { if (resultBeans.size() >= 1) { ReplayResultBean resultBean = resultBeans.get(0); // 终止adb日志 - File appLogFile = CmdTools.stopAppLog(); + File adbLogFile = CmdTools.stopAppLog(); - if (appLogFile != null && appLogFile.exists()) { - resultBean.setLogFile(appLogFile.getAbsolutePath()); + if (adbLogFile != null && adbLogFile.exists()) { + resultBean.setLogFile(adbLogFile.getAbsolutePath()); } resultBean.setScreenshotFiles(screenshotFiles); resultBean.setTargetApp(targetApp); + resultBean.setTargetAppPkg(targetAppPkg); + resultBean.setTargetAppVersion(targetAppVersionName); + resultBean.setCurrentOperationLog(stepList); resultBean.setActionLogs(currentStepInfo); resultBean.setCaseName(caseInfo.getCaseName()); if (errorReason != null) { resultBean.setExceptionStep(currentIdx - 1); + resultBean.setExceptionStepId(errorStepId); resultBean.setExceptionMessage(errorReason); } } diff --git a/src/app/src/main/java/com/alipay/hulu/replay/RepeatStepProvider.java b/src/app/src/main/java/com/alipay/hulu/replay/RepeatStepProvider.java index e6bc683..30fa40b 100644 --- a/src/app/src/main/java/com/alipay/hulu/replay/RepeatStepProvider.java +++ b/src/app/src/main/java/com/alipay/hulu/replay/RepeatStepProvider.java @@ -15,7 +15,7 @@ */ package com.alipay.hulu.replay; -import android.support.annotation.NonNull; +import androidx.annotation.NonNull; import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.bean.ReplayStepInfoBean; @@ -98,8 +98,8 @@ public boolean hasNext() { } @Override - public boolean reportErrorStep(OperationStep step, String reason) { - boolean errorResult = currentStepProvider.reportErrorStep(step, reason); + public boolean reportErrorStep(OperationStep step, String reason, List stack) { + boolean errorResult = currentStepProvider.reportErrorStep(step, reason, stack); // 如果是关键性错误 if (errorResult) { diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/ConfigSchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/ConfigSchemeResolver.java new file mode 100644 index 0000000..4d3fbd6 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/scheme/ConfigSchemeResolver.java @@ -0,0 +1,120 @@ +package com.alipay.hulu.scheme; + +import android.content.Context; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.scheme.SchemeActionResolver; +import com.alipay.hulu.common.scheme.SchemeResolver; +import com.alipay.hulu.common.service.SPService; +import com.alipay.hulu.common.utils.Callback; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; + +import java.util.Map; + +import static com.alipay.hulu.common.service.SPService.*; + +@SchemeResolver("config") +public class ConfigSchemeResolver implements SchemeActionResolver { + private static final String TAG = ConfigSchemeResolver.class.getSimpleName(); + + private static final String KEY = "key"; + private static final String VALUE = "value"; + + @Override + public boolean processScheme(Context context, Map params, Callback> callback) { + String key = params.get(KEY); + String value = params.get(VALUE); + if (StringUtil.isEmpty(key) || value == null) { + return false; + } + + return processConfigSet(key, value); + } + + /** + * 分别处理不同类型设置项 + * @param key + * @param value + * @return + */ + private boolean processConfigSet(String key, String value) { + switch (key) { + + case KEY_AUTO_CLEAR_FILES_DAYS: + return processInt(key, value, null, -1); + case KEY_SCREEN_FACTOR_ROTATION: + return processInt(key, value, 3, 0); + case KEY_SCREENSHOT_RESOLUTION: + return processInt(key, value, null, 0); + case KEY_DISPLAY_SYSTEM_APP: + case KEY_HIGHLIGHT_REPLAY_NODE: + case KEY_REPLAY_AUTO_START: + case KEY_SCREEN_ROTATION: + case KEY_RECORD_COVER_MODE: + if (StringUtil.equalsIgnoreCase(value, "true")) { + LogUtil.i(TAG, "Update Config " + key + " to value " + true); + SPService.putBoolean(key, true); + } else if (StringUtil.equalsIgnoreCase(value, "false")) { + LogUtil.i(TAG, "Update Config " + key + " to value " + false); + SPService.putBoolean(key, false); + } else { + return false; + } + break; + case KEY_CONTROL_PORT: + boolean processed = processInt(key, value, 65535, 5000); + if (processed) { + LauncherApplication.getInstance().startHttpServerAtPort(SPService.getInt(KEY_CONTROL_PORT, 23342)); + } + return processed; + case KEY_GLOBAL_SETTINGS: + JSONObject obj = JSON.parseObject(value); + if (obj == null) { + return false; + } + LogUtil.i(TAG, "Update Config " + key + " to value " + obj); + SPService.putString(key, obj.toJSONString()); + break; + case KEY_ADB_SERVER: + case KEY_PATCH_URL: + case KEY_PERFORMANCE_UPLOAD: + LogUtil.i(TAG, "Update Config " + key + " to value " + value); + SPService.putString(key, value); + break; + } + return true; + } + + /** + * 处理数字 + * @param key + * @param value + * @param max + * @param min + * @return + */ + private boolean processInt(String key, String value, Integer max, Integer min) { + try { + int val = Integer.parseInt(value); + if (max != null && val > max) { + LogUtil.w(TAG, "Value " + value + " bigger than max value: " + max + " for key " + key); + return false; + } + + if (min != null && val < min) { + LogUtil.w(TAG, "Value " + value + " smaller than min value: " + min + " for key " + key); + return false; + } + + LogUtil.i(TAG, "Update Config " + key + " to value " + val); + SPService.putInt(key, val); + return true; + } catch (NumberFormatException e) { + LogUtil.e(TAG, "Can't parse int value " + value + " for key " + key, e); + return false; + } + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/PerformanceSchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/PerformanceSchemeResolver.java new file mode 100644 index 0000000..bfe005b --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/scheme/PerformanceSchemeResolver.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.scheme; + +import android.app.Activity; +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.provider.Settings; + +import com.alipay.hulu.R; +import com.alipay.hulu.activity.MyApplication; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.scheme.SchemeActionResolver; +import com.alipay.hulu.common.scheme.SchemeResolver; +import com.alipay.hulu.common.tools.AppInfoProvider; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.Callback; +import com.alipay.hulu.common.utils.PermissionUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.screenRecord.Notifications; +import com.alipay.hulu.shared.display.DisplayItemInfo; +import com.alipay.hulu.shared.display.DisplayProvider; +import com.alipay.hulu.shared.display.items.base.RecordPattern; +import com.alipay.hulu.util.RecordUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Created by qiaoruikai on 2019/12/4 4:47 PM. + */ +@SchemeResolver("performance") +public class PerformanceSchemeResolver implements SchemeActionResolver { + private static final String PERFORMANCE_MODE = "mode"; + private static final String MODE_NORMAL = "normal"; + private static final String TARGET_APP = "targetApp"; + private static final String NORMAL_ITEMS = "items"; + private static final String REPORT_URL = "url"; + private static final String ACTION = "action"; + + private Notification notification; + private static final int PERFORMANCE_RECORD_ID = 12201; + private boolean isRecording = false; + + @Override + public boolean processScheme(Context context, Map params, Callback> callback) { + String mode = params.get(PERFORMANCE_MODE); + if (StringUtil.isEmpty(mode)) { + return false; + } + + switch (mode) { + case MODE_NORMAL: + return processNormalRecord(context, params); + default: + return false; + } + } + + /** + * 处理正常性能录制 + * @param context + * @param params + * @return + */ + private boolean processNormalRecord(final Context context, Map params) { + String action = params.get(ACTION); + if (StringUtil.equals(action, "start")) { + String itemList = params.get(NORMAL_ITEMS); + final String[] itemArray = StringUtil.split(itemList, ","); + if (itemArray == null) { + return false; + } + + // 调整待测应用 + String targetApp = params.get(TARGET_APP); + if (!StringUtil.isEmpty(targetApp)) { + String appLabel = null; + List appList = MyApplication.getInstance().loadAppList(); + for (ApplicationInfo appInfo : appList) { + if (StringUtil.equals(appInfo.packageName, targetApp)) { + appLabel = appInfo.loadLabel(context.getPackageManager()).toString(); + } + } + // 没找到对应应用 + if (StringUtil.isEmpty(appLabel)) { + return false; + } + + // 更新待测应用 + MyApplication.getInstance().updateAppAndNameTemp(targetApp, appLabel); + } + + final List items = Arrays.asList(itemArray); + final DisplayProvider displayProvider = LauncherApplication.service(DisplayProvider.class); + // 逐项开启 + List displayItems = displayProvider.getAllDisplayItems(); + Set allPermissions = new HashSet<>(); + for (DisplayItemInfo info: displayItems) { + if (items.contains(info.getKey())) { + allPermissions.addAll(info.getPermissions()); + } + } + allPermissions.add("adb"); + allPermissions.add("powerSave"); + + PermissionUtil.requestPermissions(new ArrayList<>(allPermissions), (Activity) context, new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(boolean result, String reason) { + if (result) { + isRecording = true; + + AppInfoProvider provider = AppInfoProvider.getInstance(); + InjectorService.g().unregister(provider); + InjectorService.g().register(provider); + + // 逐项开启 + displayProvider.stopAllDisplay(); + for (String key : items) { + displayProvider.startDisplay(key); + } + displayProvider.startRecording(); + notification = Notifications.generateNotificationBuilder(context) + .setContentTitle(context.getString(R.string.performance__recording)) + .setOngoing(true) + .setOnlyAlertOnce(true) + .setWhen(System.currentTimeMillis()) + .setPriority(Notification.PRIORITY_HIGH) + .setSmallIcon(R.drawable.icon_recording) + .setUsesChronometer(true) + .setContentText(context.getString(R.string.performance__recording_performance_data)) + .build(); + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(PERFORMANCE_RECORD_ID, notification); + } else { + LauncherApplication.getInstance().showToast(context.getString(R.string.performance__start_performance_recording_fail)); + } + } + }); + } else if (StringUtil.equals(action, "stop")) { + final String reportUrl = params.get(REPORT_URL); + if (!isRecording) { + return false; + } + + // 清理通知 + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(PERFORMANCE_RECORD_ID); + notification = null; + + DisplayProvider displayProvider = LauncherApplication.getInstance().findServiceByName(DisplayProvider.class.getName()); + final Map> records = displayProvider.stopRecording(); + displayProvider.stopAllDisplay(); + + isRecording = false; + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + if (StringUtil.isEmpty(reportUrl)) { + // 存储录制数据 + File folder = RecordUtil.saveToFile(records); + + // 显示提示框 + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.performance__record_save, folder.getPath())); + } else { + String response = RecordUtil.uploadData(reportUrl, records); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.performance__record_upload, reportUrl, response)); + } + } + }); + + } + + return true; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/RecordSchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/RecordSchemeResolver.java new file mode 100644 index 0000000..7f7b2a9 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/scheme/RecordSchemeResolver.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.scheme; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.provider.Settings; + +import com.alipay.hulu.activity.MyApplication; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.scheme.SchemeActionResolver; +import com.alipay.hulu.common.scheme.SchemeResolver; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.Callback; +import com.alipay.hulu.common.utils.PermissionUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.service.CaseRecordManager; +import com.alipay.hulu.shared.io.OperationStepService; +import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.io.db.OperationLogHandler; +import com.alipay.hulu.shared.node.utils.PrepareUtil; +import com.alipay.hulu.util.DialogUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Created by qiaoruikai on 2019/11/11 11:47 AM. + */ +@SchemeResolver("record") +public class RecordSchemeResolver implements SchemeActionResolver { + public static final String RECORD_MODE = "recordMode"; + public static final String CASE_NAME = "caseName"; + public static final String CASE_DESC = "caseDesc"; + public static final String TARGET_APP = "targetApp"; + + public static final String MODE_NORMAL = "normal"; + + @Override + public boolean processScheme(Context context, Map params, Callback> callback) { + String mode = params.get(RECORD_MODE); + if (StringUtil.isEmpty(mode)) { + return false; + } + + switch (mode) { + case MODE_NORMAL: + return startNormalMode(context, params); + } + return false; + } + + /** + * 通常模式启动录制 + * @param context + * @param params + * @return + */ + private boolean startNormalMode(final Context context, Map params) { + final RecordCaseInfo caseInfo = loadBaseInfo(context, params); + if (caseInfo == null) { + return false; + } + caseInfo.setRecordMode("local"); + + PermissionUtil.requestPermissions(Arrays.asList("adb", "float", Settings.ACTION_ACCESSIBILITY_SETTINGS, "powerSave"), (Activity) context, new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(boolean result, String reason) { + if (result) { + + final ProgressDialog dialog = DialogUtils.showProgressDialog(LauncherApplication.getContext(), "正在加载中"); + MyApplication.getInstance().updateAppAndName(caseInfo.getTargetAppPackage(), caseInfo.getTargetAppLabel()); + + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + boolean prepareResult = PrepareUtil.doPrepareWork(caseInfo.getTargetAppPackage(), new PrepareUtil.PrepareStatus() { + @Override + public void currentStatus(int progress, int total, String message, boolean status) { + updateProgressDialog(dialog, progress, total, message); + } + }); + + if (prepareResult) { + dismissProgressDialog(dialog); + + LauncherApplication.service(OperationStepService.class).registerStepProcessor(new OperationLogHandler()); + CaseRecordManager caseRecordManager = LauncherApplication.service(CaseRecordManager.class); + caseRecordManager.setRecordCase(caseInfo); + } else { + dismissProgressDialog(dialog); + LauncherApplication.getInstance().showToast("环境加载失败"); + } + } + }); + } + } + }); + + return true; + } + + private static RecordCaseInfo loadBaseInfo(Context context, Map params) { + if (params == null) { + return null; + } + String app = params.get(TARGET_APP); + if (StringUtil.isEmpty(app)) { + return null; + } + String appLabel = null; + List appList = MyApplication.getInstance().loadAppList(); + for (ApplicationInfo appInfo: appList) { + if (StringUtil.equals(appInfo.packageName, app)) { + appLabel = appInfo.loadLabel(context.getPackageManager()).toString(); + } + } + // 没找到对应应用 + if (StringUtil.isEmpty(appLabel)) { + return null; + } + + RecordCaseInfo caseInfo = new RecordCaseInfo(); + caseInfo.setCaseName(params.get(CASE_NAME)); + caseInfo.setCaseDesc(params.get(CASE_DESC)); + caseInfo.setTargetAppPackage(app); + caseInfo.setTargetAppLabel(appLabel); + return caseInfo; + } + + public void dismissProgressDialog(final ProgressDialog progressDialog) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + public void run() { + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + } + }); + } + + public void updateProgressDialog(final ProgressDialog progressDialog, final int progress, final int totalProgress, final String message) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (progressDialog == null || !progressDialog.isShowing()) { + return; + } + + // 更新progressDialog的状态 + progressDialog.setProgress(progress); + progressDialog.setMax(totalProgress); + progressDialog.setMessage(message); + } + }); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/ReplaySchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/ReplaySchemeResolver.java new file mode 100644 index 0000000..42386d1 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/scheme/ReplaySchemeResolver.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.scheme; + +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.provider.Settings; + +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.scheme.SchemeActionResolver; +import com.alipay.hulu.common.scheme.SchemeResolver; +import com.alipay.hulu.common.utils.Callback; +import com.alipay.hulu.common.utils.PermissionUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.io.db.GreenDaoManager; +import com.alipay.hulu.shared.io.db.RecordCaseInfoDao; +import com.alipay.hulu.util.CaseReplayUtil; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Created by qiaoruikai on 2019/11/11 4:57 PM. + */ +@SchemeResolver("replay") +public class ReplaySchemeResolver implements SchemeActionResolver { + public static final String REPLAY_MODE = "replayMode"; + public static final String CASE_NAME = "caseName"; + public static final String TARGET_APP = "targetApp"; + + public static final String MODE_NORMAL = "normal"; + + @Override + public boolean processScheme(Context context, Map params, Callback> callback) { + String mode = params.get(REPLAY_MODE); + if (StringUtil.isEmpty(mode)) { + return false; + } + + switch (mode) { + case MODE_NORMAL: + return startNormalMode(context, params); + } + return false; + } + + /** + * 通常模式启动录制 + * @param context + * @param params + * @return + */ + private boolean startNormalMode(final Context context, Map params) { + String caseName = params.get(CASE_NAME); + if (StringUtil.isEmpty(caseName)) { + return false; + } + + List caseInfos = GreenDaoManager.getInstance().getRecordCaseInfoDao().queryBuilder() + .where(RecordCaseInfoDao.Properties.CaseName.eq(caseName)) + .orderDesc(RecordCaseInfoDao.Properties.Id).limit(1).list(); + if (caseInfos == null || caseInfos.size() < 1) { + return false; + } + final RecordCaseInfo caseInfo = caseInfos.get(0); + PermissionUtil.requestPermissions(Arrays.asList("adb", "float", "background", Settings.ACTION_ACCESSIBILITY_SETTINGS), (Activity) context, new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(final boolean result, String reason) { + if (result) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + CaseReplayUtil.startReplay(caseInfo); + } + }); + } + } + }); + return true; + } + + public void dismissProgressDialog(final ProgressDialog progressDialog) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + public void run() { + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + } + }); + } + + public void updateProgressDialog(final ProgressDialog progressDialog, final int progress, final int totalProgress, final String message) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (progressDialog == null || !progressDialog.isShowing()) { + return; + } + + // 更新progressDialog的状态 + progressDialog.setProgress(progress); + progressDialog.setMax(totalProgress); + progressDialog.setMessage(message); + } + }); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/scheme/StatusSchemeResolver.java b/src/app/src/main/java/com/alipay/hulu/scheme/StatusSchemeResolver.java new file mode 100644 index 0000000..aa7f6d1 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/scheme/StatusSchemeResolver.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.scheme; + +import android.content.Context; +import android.provider.Settings; +import android.util.Log; + +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.common.scheme.SchemeActionResolver; +import com.alipay.hulu.common.scheme.SchemeResolver; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.Callback; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.PermissionUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.shared.node.OperationService; +import com.alipay.hulu.shared.node.tree.AbstractNodeTree; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.alipay.hulu.shared.event.constant.Constant.RUNNING_STATUS; + +@SchemeResolver("status") +public class StatusSchemeResolver implements SchemeActionResolver { + private static final String TAG = StatusSchemeResolver.class.getSimpleName(); + + public static final String KEY_STATUS_TYPE = "type"; + public static final String KEY_STATUS = "status"; + public static final String KEY_PAGE = "page"; + + public StatusSchemeResolver() { + InjectorService.g().register(this); + } + + private String currentStatus = "none"; + + @Subscriber(@Param(RUNNING_STATUS)) + public void setCurrentStatus(String currentStatus) { + this.currentStatus = currentStatus; + } + + @Override + public boolean processScheme(Context context, Map params, final Callback> callback) { + String type = params.get(KEY_STATUS_TYPE); + if (StringUtil.isEmpty(type)) { + return false; + } + + LogUtil.i(TAG, "Status Scheme处理中,请求参数:" + params); + switch (type) { + case KEY_STATUS: + callback.onResult(Collections.singletonMap("status", currentStatus)); + return true; + case KEY_PAGE: + boolean isGranted = PermissionUtil.getPermissionStatus(context, "adb") && PermissionUtil.getPermissionStatus(context, Settings.ACTION_ACCESSIBILITY_SETTINGS); + if (!isGranted) { + final AtomicBoolean permissionResult = new AtomicBoolean(false); + final CountDownLatch latch = new CountDownLatch(1); + PermissionUtil.requestPermissions(Arrays.asList("adb", Settings.ACTION_ACCESSIBILITY_SETTINGS), LauncherApplication.getInstance().getBestForegroundContext(), new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(boolean result, String reason) { + permissionResult.set(result); + latch.countDown(); + } + }); + + try { + latch.await(); + } catch (InterruptedException e) { + Log.e(TAG, "等待权限授予失败", e); + } + if (!permissionResult.get()) { + callback.onResult(Collections.singletonMap("error", "未授予权限")); + return true; + } + } + // 等500ms后再加载页面信息 + final CountDownLatch getNodeLatch = new CountDownLatch(1); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + OperationService service = LauncherApplication.service(OperationService.class); + AbstractNodeTree root = service.getBaseCurrentRoot(); + + // 构造可传输的树结构 + JSONObject obj = root.exportToJsonObject(); + + callback.onResult(Collections.singletonMap("page", obj)); + service.invalidRoot(); + getNodeLatch.countDown(); + } + }, 500); + try { + getNodeLatch.await(); + } catch (InterruptedException e) { + LogUtil.e(TAG, "Load node failed", e); + } + + return true; + } + + return false; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/screenRecord/Notifications.java b/src/app/src/main/java/com/alipay/hulu/screenRecord/Notifications.java index 49f0ade..f7e71aa 100644 --- a/src/app/src/main/java/com/alipay/hulu/screenRecord/Notifications.java +++ b/src/app/src/main/java/com/alipay/hulu/screenRecord/Notifications.java @@ -17,6 +17,7 @@ import android.annotation.TargetApi; import android.app.Notification; +import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; @@ -34,6 +35,7 @@ */ @TargetApi(value = Build.VERSION_CODES.LOLLIPOP) public class Notifications extends ContextWrapper { + private static final String HULU_NOTIFICATIONS_CHANNEL_ID = "hulu-notifications"; private static final int id = 0x1fff; private static final String ACTION_STOP = "com.hulu.alipay.ACTION_STOP"; @@ -47,22 +49,22 @@ public Notifications(Context context) { } public void recording(long timeMs) { - if (SystemClock.elapsedRealtime() - mLastFiredTime < 1000) { - return; - } - - //隐藏所有消息 - getNotificationManager().cancelAll(); - Notification notification = getBuilder() - .setContentText("Length: " + DateUtils.formatElapsedTime(timeMs / 1000)) - .build(); - getNotificationManager().notify(id, notification); - mLastFiredTime = SystemClock.elapsedRealtime(); +// if (SystemClock.elapsedRealtime() - mLastFiredTime < 1000) { +// return; +// } +// +// //隐藏所有消息 +// getNotificationManager().cancelAll(); +// Notification notification = getBuilder() +// .setContentText("Length: " + DateUtils.formatElapsedTime(timeMs / 1000)) +// .build(); +// getNotificationManager().notify(id, notification); +// mLastFiredTime = SystemClock.elapsedRealtime(); } private Notification.Builder getBuilder() { if (mBuilder == null) { - mBuilder = new Notification.Builder(this) + mBuilder = generateNotificationBuilder(this) .setContentTitle("屏幕录制中") .setOngoing(true) .setLocalOnly(true) @@ -75,6 +77,20 @@ private Notification.Builder getBuilder() { return mBuilder; } + public static Notification.Builder generateNotificationBuilder(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String channelId = HULU_NOTIFICATIONS_CHANNEL_ID; + NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_DEFAULT); + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (nm.getNotificationChannel(channelId) == null) { + nm.createNotificationChannel(channel); + } + return new Notification.Builder(context, channelId); + } else { + return new Notification.Builder(context); + } + } + private Notification.Action stopAction() { if (mStopAction == null) { Intent intent = new Intent(ACTION_STOP).setPackage(getPackageName()); diff --git a/src/app/src/main/java/com/alipay/hulu/screenRecord/RecordService.java b/src/app/src/main/java/com/alipay/hulu/screenRecord/RecordService.java index 47c6547..6a201f0 100644 --- a/src/app/src/main/java/com/alipay/hulu/screenRecord/RecordService.java +++ b/src/app/src/main/java/com/alipay/hulu/screenRecord/RecordService.java @@ -16,6 +16,7 @@ package com.alipay.hulu.screenRecord; import android.annotation.TargetApi; +import android.app.Notification; import android.app.Service; import android.content.Context; import android.content.Intent; @@ -26,15 +27,18 @@ import android.os.Build; import android.os.Handler; import android.os.IBinder; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; +import android.widget.AdapterView; import android.widget.ImageView; +import android.widget.ListView; +import android.widget.SimpleAdapter; import android.widget.TextView; import com.alipay.hulu.R; @@ -43,20 +47,29 @@ import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.injector.param.Subscriber; import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.tools.CmdTools; +import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.MiscUtil; +import com.alipay.hulu.service.BaseService; import com.alipay.hulu.shared.event.EventService; import com.alipay.hulu.shared.event.bean.UniversalEventBean; import com.alipay.hulu.shared.event.constant.Constant; import java.io.File; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; @TargetApi(value = Build.VERSION_CODES.LOLLIPOP) -public class RecordService extends Service { +public class RecordService extends BaseService { + public static final int RECORD_SERVICE_NOTIFICATION_ID = 26543; public static final String INTENT_RESULT_CODE = "INTENT_RESULT_CODE"; public static final String INTENT_VIDEO_CODEC = "INTENT_VIDEO_CODEC"; @@ -76,8 +89,11 @@ public class RecordService extends Service { private View view; private TextView recordBtn; - private ImageView closeBtn; - private TextView resultView; + private View closeBtn; + private ListView resultList; + private TextView killCurrent; + private SimpleAdapter adapter; + private ImageView resultHide; private float mTouchStartX; private float mTouchStartY; @@ -88,6 +104,9 @@ public class RecordService extends Service { private long lastMotionDownTime; + private List results; + private List> displayDataSource; + private String mCodec; private int mFrameRate; private int mBitrate; @@ -107,6 +126,8 @@ public class RecordService extends Service { private boolean isCalculating = false; private VideoEncodeConfig mVideo; + private boolean hideResult = false; + private Handler mHandler; private MediaProjection mMediaProjection; @@ -121,8 +142,13 @@ public class RecordService extends Service { public void onCreate() { super.onCreate(); LogUtil.d(TAG, "onCreate"); + results = new ArrayList<>(); createView(); + Notification notification = generateNotificationBuilder().setContentText(getString(R.string.service_notification__solopi_record_running)).setSmallIcon(R.drawable.solopi_main).build(); + startForeground(RECORD_SERVICE_NOTIFICATION_ID, notification); + + mMediaProjectionManager = (MediaProjectionManager)getSystemService(Context.MEDIA_PROJECTION_SERVICE); mNotifications = new Notifications(getApplicationContext()); mHandler = new Handler(); @@ -141,13 +167,68 @@ public IBinder onBind(Intent intent) { private void createView() { - view = LayoutInflater.from(this).inflate(R.layout.record_service, null); + view = LayoutInflater.from(ContextUtil.getContextThemeWrapper(this, R.style.AppTheme)).inflate(R.layout.record_service, null); recordBtn = (TextView) view.findViewById(R.id.record_btn); - recordBtn.setText("开始录制"); - closeBtn = (ImageView) view.findViewById(R.id.close_btn); - resultView = (TextView) view.findViewById(R.id.result); - resultView.setVisibility(View.GONE); + recordBtn.setText(R.string.record__start_record); + closeBtn = view.findViewById(R.id.close_btn); + resultList = (ListView) view.findViewById(R.id.record_session_result); + killCurrent = (TextView) view.findViewById(R.id.record_kill_current); + resultHide = (ImageView) view.findViewById(R.id.record_session_hide); + + displayDataSource = new ArrayList<>(); + adapter = new SimpleAdapter(this, displayDataSource, R.layout.item_screen_result, new String[] {"title", "value"}, new int[] {R.id.screen_result_title, R.id.screen_result_value}); + resultList.setAdapter(adapter); + resultList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (position < results.size()) { + removeResultAt(position); + } else { + clearResult(); + } + } + }); + + killCurrent.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + if (CmdTools.isInitialized()) { + String[] pA = CmdTools.getTopPkgAndActivity(); + if (pA == null || pA.length != 2) { + LauncherApplication.getInstance().showToast("获取当前应用失败"); + return; + } + LogUtil.i(TAG, "当前应用: %s, 当前Activity: %s", pA[0], pA[1]); + + // 杀两遍 + CmdTools.execHighPrivilegeCmd("am force-stop " + pA[0]); + CmdTools.execHighPrivilegeCmd("am force-stop " + pA[0]); + } else { + // 申请ADB + requestAdb(); + } + } + }); + } + }); + + resultHide.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideResult = !hideResult; + if (hideResult) { + resultList.setVisibility(View.GONE); + resultHide.setRotation(0); + } else { + resultList.setVisibility(View.VISIBLE); + resultHide.setRotation(180); + } + } + }); if (statusBarHeight == 0) { try { @@ -167,7 +248,7 @@ private void createView() { wm = (WindowManager) getApplicationContext().getSystemService(WINDOW_SERVICE); wmParams = ((MyApplication)getApplication()).getFloatWinParams(); - wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; + wmParams.type = com.alipay.hulu.common.constant.Constant.TYPE_ALERT; wmParams.flags |= 8; wmParams.gravity = Gravity.LEFT | Gravity.TOP; // 调整悬浮窗口至左上角 // 以屏幕左上角为原点,设置x、y初始值 @@ -188,7 +269,7 @@ private void createView() { @Override public void run() { closeBtn.getHitRect(closeRect); - recordBtn.getGlobalVisibleRect(recordRect); + recordBtn.getHitRect(recordRect); } }, 500); @@ -224,9 +305,11 @@ public boolean onTouch(View v, MotionEvent event) { if (closeRect.contains((int)curX, (int)curY) && closeRect.contains((int)mTouchStartX, (int)mTouchStartY)) { + LogUtil.i(TAG, "Click Close Btn"); onCloseBtnClicked(); } else if (recordRect.contains((int)curX, (int)curY) && recordRect.contains((int)mTouchStartX, (int)mTouchStartY)) { + LogUtil.i(TAG, "Click Record Btn"); onRecordBtnClicked(); } } @@ -243,6 +326,31 @@ public boolean onTouch(View v, MotionEvent event) { view.setAlpha(0.8f); } + private void requestAdb() { + LauncherApplication.getInstance().showDialog(RecordService.this, "ADB连接尚未开启,是否开启?", "开启", new Runnable() { + @Override + public void run() { + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + boolean result; + try { + result = CmdTools.generateConnection(); + } catch (Exception e) { + LogUtil.e(TAG, "连接adb异常", e); + result = false; + } + if (result) { + LauncherApplication.getInstance().showToast("开启成功"); + } else { + LauncherApplication.getInstance().showToast("开启失败"); + } + } + }); + } + }, "取消", null); + } + private void onRecordBtnClicked() { if (isCalculating) { return; @@ -274,7 +382,8 @@ private void updateViewPosition() { @Override public int onStartCommand(Intent intent, int flags, int startId) { LogUtil.d(TAG, "onStart"); - stopForeground(false); +// Notification notification = new Notification.Builder(this).setContentText(getString(R.string.float__toast_title)).setSmallIcon(R.drawable.solopi_main).build(); +// startForeground(NOTIFICATION_ID, notification); if (intent == null) { return super.onStartCommand(intent, flags, startId); @@ -343,10 +452,88 @@ private File generateVideoPath() { return file; } + /** + * 增加结果列 + * @param result + */ + private void addResultValue(long result) { + results.add(result); + displayDataSource.clear(); + long total = 0; + for (int i = 0; i < results.size(); i++) { + long val = results.get(i); + total += val; + Map display = new HashMap<>(3); + display.put("title", getString(R.string.record_float__nth_time, i + 1)); + display.put("value", val + "ms"); + displayDataSource.add(display); + } + + Map display = new HashMap<>(3); + display.put("title", getString(R.string.record_float__average)); + display.put("value", (total / results.size()) + "ms"); + displayDataSource.add(display); + + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + adapter.notifyDataSetChanged(); + } + }); + } + + /** + * 清空结果列 + */ + private void clearResult() { + results.clear(); + displayDataSource.clear(); + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + adapter.notifyDataSetChanged(); + } + }); + } + + /** + * 删除结果列特定项 + */ + private void removeResultAt(int position) { + if (position >= results.size() || position < 0) { + return; + } + results.remove(position); + displayDataSource.clear(); + long total = 0; + for (int i = 0; i < results.size(); i++) { + long val = results.get(i); + total += val; + Map display = new HashMap<>(3); + display.put("title", "第" + (i + 1) + "次"); + display.put("value", val + "ms"); + displayDataSource.add(display); + } + + if (results.size() > 0) { + Map display = new HashMap<>(3); + display.put("title", "平均值"); + display.put("value", (total / results.size()) + "ms"); + displayDataSource.add(display); + } + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + adapter.notifyDataSetChanged(); + } + }); + } + @Override public void onDestroy() { LogUtil.d(TAG, "onDestroy"); wm.removeView(view); + stopForeground(true); injectorService.unregister(this); injectorService = null; @@ -373,12 +560,11 @@ public void onStop(Throwable error) { public void run() { isRecording = false; isCalculating = true; - recordBtn.setText("正在计算"); - resultView.setText("请稍候..."); - resultView.setVisibility(View.VISIBLE); + recordBtn.setText(R.string.record__calculating); + LauncherApplication.getInstance().showToast(getString(R.string.record__please_wait)); } }); - mHandler.postDelayed(new Runnable() { + BackgroundExecutor.execute(new Runnable() { @Override public void run() { VideoAnalyzer.getInstance().doAnalyze(lastCalculateT1,video.exceptDiff @@ -389,11 +575,11 @@ public void onAnalyzeFinished(final long result) { @Override public void run() { isCalculating = false; - recordBtn.setText("开始录制"); + recordBtn.setText(R.string.record__start_record); if (result <= 0) { - resultView.setText("操作过快,请重试"); + LauncherApplication.getInstance().showToast(getString(R.string.record__operation_fast)); } else { - resultView.setText(getString(R.string.record_service__cost_time, result)); + addResultValue(result); } } }); @@ -405,8 +591,8 @@ public void onAnalyzeFailed(final String msg) { @Override public void run() { isCalculating = false; - recordBtn.setText("开始录制"); - resultView.setText(msg); + recordBtn.setText(R.string.record__start_record); + LauncherApplication.getInstance().showToast(msg); } }); } @@ -429,8 +615,7 @@ public void onStart() { public void run() { isRecording = true; hasClicked = false; - recordBtn.setText("结束录制"); - resultView.setVisibility(View.GONE); + recordBtn.setText(R.string.record__stop_record); mNotifications.recording(0); } }); diff --git a/src/app/src/main/java/com/alipay/hulu/screenRecord/RecorderConfigActivity.java b/src/app/src/main/java/com/alipay/hulu/screenRecord/RecorderConfigActivity.java index 6b4c40d..5bf4d73 100644 --- a/src/app/src/main/java/com/alipay/hulu/screenRecord/RecorderConfigActivity.java +++ b/src/app/src/main/java/com/alipay/hulu/screenRecord/RecorderConfigActivity.java @@ -109,7 +109,7 @@ protected void onDestroy() { private void initViews() { mPanel = (HeadControlPanel) findViewById(R.id.info_head); - mPanel.setMiddleTitle("录屏设置"); + mPanel.setMiddleTitle(getString(R.string.activity__record_config)); mPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -124,7 +124,7 @@ public void onClick(View v) { if (checkVideoSettings()) { startWindow(v); } else { - toastShort("视频参数不支持"); + toastShort(getString(R.string.codec__video_config_unsupport)); } } }); @@ -297,15 +297,15 @@ private void onResolutionChanged(int selectedPosition, String resolution) { int resetPos = Math.max(selectedPosition - 1, 0); if (!videoCapabilities.isSizeSupported(width, height)) { mVideoResolution.setSelectedPosition(resetPos); - toastShort("codec '%s' unsupported size %dx%d ", - codecName, width, height); + toastShort(getString(R.string.codec_config__unsupport_size, + codecName, width, height)); LogUtil.w(TAG, codecName + " height range: " + videoCapabilities.getSupportedHeights() + "\n width range: " + videoCapabilities.getSupportedHeights()); } else if (!videoCapabilities.areSizeAndRateSupported(width, height, selectedFramerate)) { mVideoResolution.setSelectedPosition(resetPos); - toastShort("codec '%s' unsupported size %dx%d\nwith framerate %d", - codecName, width, height, (int) selectedFramerate); + toastShort(getString(R.string.codec_config__unsupport_size_framerate, + codecName, width, height, (int) selectedFramerate)); } } @@ -320,7 +320,7 @@ private void onBitrateChanged(int selectedPosition, String bitrate) { int resetPos = Math.max(selectedPosition - 1, 0); if (!videoCapabilities.getBitrateRange().contains(selectedBitrate)) { mVideoBitrate.setSelectedPosition(resetPos); - toastShort("codec '%s' unsupported bitrate %d", codecName, selectedBitrate); + toastShort(getString(R.string.codec_config__unsupport_bitrate, codecName, selectedBitrate)); LogUtil.w(TAG, codecName + " bitrate range: " + videoCapabilities.getBitrateRange()); } @@ -345,11 +345,11 @@ private void onFramerateChanged(int selectedPosition, String rate) { int resetPos = Math.max(selectedPosition - 1, 0); if (!videoCapabilities.getSupportedFrameRates().contains(selectedFramerate)) { mVideoFrameRate.setSelectedPosition(resetPos); - toastShort("codec '%s' unsupported framerate %d", codecName, selectedFramerate); + toastShort(getString(R.string.codec_config__unsupport_framerate, codecName, selectedFramerate)); } else if (!videoCapabilities.areSizeAndRateSupported(width, height, selectedFramerate)) { mVideoFrameRate.setSelectedPosition(resetPos); - toastShort("codec '%s' unsupported size %dx%d\nwith framerate %d", - codecName, width, height, selectedFramerate); + toastShort(getString(R.string.codec_config__unsupport_size_framerate, + codecName, width, height, selectedFramerate)); } } @@ -390,8 +390,8 @@ private String getSelectedVideoCodec() { } private SpinnerAdapter createCodecsAdapter(MediaCodecInfo[] codecInfos) { - ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, codecInfoNames(codecInfos)); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + ArrayAdapter adapter = new ArrayAdapter<>(this, R.layout.auto_size_spinner_item, codecInfoNames(codecInfos)); + adapter.setDropDownViewResource(R.layout.auto_size_spinner_dropdown_item); return adapter; } diff --git a/src/app/src/main/java/com/alipay/hulu/screenRecord/ScreenRecorder.java b/src/app/src/main/java/com/alipay/hulu/screenRecord/ScreenRecorder.java index 31b2a2d..371754e 100644 --- a/src/app/src/main/java/com/alipay/hulu/screenRecord/ScreenRecorder.java +++ b/src/app/src/main/java/com/alipay/hulu/screenRecord/ScreenRecorder.java @@ -28,6 +28,7 @@ import android.os.Looper; import android.os.Message; +import com.alipay.hulu.BuildConfig; import com.alipay.hulu.common.utils.LogUtil; import java.io.IOException; @@ -41,7 +42,7 @@ @TargetApi(value = Build.VERSION_CODES.LOLLIPOP) public class ScreenRecorder { private static final String TAG = "ScreenRecorder"; - private static final boolean VERBOSE = false; + private static final boolean VERBOSE = BuildConfig.DEBUG; private static final int INVALID_INDEX = -1; public static final String VIDEO_AVC = MediaFormat.MIMETYPE_VIDEO_AVC; // H.264 Advanced Video Coding private int mWidth; diff --git a/src/app/src/main/java/com/alipay/hulu/screenRecord/SimpleRecordService.java b/src/app/src/main/java/com/alipay/hulu/screenRecord/SimpleRecordService.java index a085431..9b337b2 100644 --- a/src/app/src/main/java/com/alipay/hulu/screenRecord/SimpleRecordService.java +++ b/src/app/src/main/java/com/alipay/hulu/screenRecord/SimpleRecordService.java @@ -16,7 +16,7 @@ package com.alipay.hulu.screenRecord; import android.annotation.TargetApi; -import android.app.Service; +import android.app.Notification; import android.content.Context; import android.content.Intent; import android.media.MediaCodecInfo; @@ -26,11 +26,14 @@ import android.os.Build; import android.os.Handler; import android.os.IBinder; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import android.view.WindowManager; +import com.alipay.hulu.R; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.service.BaseService; import com.alipay.hulu.util.VideoUtils; import java.io.File; @@ -39,22 +42,33 @@ import java.util.Date; import java.util.Locale; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import static android.app.Activity.RESULT_OK; /** * Created by qiaoruikai on 2019/1/9 3:31 PM. */ @TargetApi(value = Build.VERSION_CODES.LOLLIPOP) -public class SimpleRecordService extends Service { +public class SimpleRecordService extends BaseService { + private static final int RECORD_SERVICE_NOTIFICATION_ID = 36231; public static final String INTENT_WIDTH = "INTENT_WIDTH"; public static final String INTENT_HEIGHT = "INTENT_HEIGHT"; public static final String INTENT_FRAME_RATE = "INTENT_FRAME_RATE"; public static final String INTENT_VIDEO_BITRATE = "INTENT_VIDEO_BITRATE"; public static final String INTENT_EXCEPT_DIFF = "INTENT_EXCEPT_DIFF"; + public static final String VIDEO_DIR = "ScreenCaptures"; + + private static final String TAG = SimpleRecordService.class.getSimpleName(); + private static final int NOTIFICATION_ID = 19222; - private static final String TAG = RecordService.class.getSimpleName(); - private static final String VIDEO_DIR = "ScreenCaptures"; + private static final int TYPE_TOAST = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY: WindowManager.LayoutParams.TYPE_TOAST; + + static { + LauncherApplication.getInstance().registerSelfAsForegroundService(SimpleRecordService.class); + } private boolean isRecording; private MediaProjectionManager mMediaProjectionManager; @@ -73,8 +87,13 @@ public class SimpleRecordService extends Service { @Override public void onCreate() { super.onCreate(); + + Notification notification = generateNotificationBuilder().setContentText(getString(R.string.service_notification__solopi_record_running)).setSmallIcon(R.drawable.solopi_main).build(); + startForeground(RECORD_SERVICE_NOTIFICATION_ID, notification); + mHandler = new Handler(); LogUtil.d(TAG, "onCreate"); + InjectorService.g().register(this); mMediaProjectionManager = (MediaProjectionManager)getSystemService(Context.MEDIA_PROJECTION_SERVICE); mNotifications = new Notifications(getApplicationContext()); @@ -83,13 +102,13 @@ public void onCreate() { @Nullable @Override public IBinder onBind(Intent intent) { - return new RecordBinder(this); + return new SimpleRecordService.RecordBinder(this); } @Override public int onStartCommand(Intent intent, int flags, int startId) { LogUtil.d(TAG, "onStart"); - stopForeground(false); +// stopForeground(false); return super.onStartCommand(intent, flags, startId); } @@ -145,9 +164,10 @@ private File generateVideoPath() { @Override public void onDestroy() { - LogUtil.d(TAG, "onDestroy"); - super.onDestroy(); + stopForeground(false); + + LogUtil.d(TAG, "onDestroy"); } @@ -204,6 +224,7 @@ private long stopRecorder() { return lastRecorderStartTime; } + private VideoEncodeConfig createVideoConfig(Intent intent) { // 不同系统,不同硬件,codec不一样,无法传递 MediaCodecInfo[] codecs = VideoUtils.findEncodersByType(ScreenRecorder.VIDEO_AVC); @@ -249,5 +270,9 @@ public File startRecord(Intent intent) { public long stopRecord() { return recordRef.get().stopRecorder(); } + + public Context loadContext() { + return recordRef.get(); + } } } \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/screenRecord/TextSpinner.java b/src/app/src/main/java/com/alipay/hulu/screenRecord/TextSpinner.java index 015f782..dd4e434 100644 --- a/src/app/src/main/java/com/alipay/hulu/screenRecord/TextSpinner.java +++ b/src/app/src/main/java/com/alipay/hulu/screenRecord/TextSpinner.java @@ -68,18 +68,18 @@ public void onNothingSelected(AdapterView parent) { final CharSequence[] entries = a.getTextArray(R.styleable.TextSpinner_entries); if (entries != null) { final ArrayAdapter adapter = new ArrayAdapter<>( - context, android.R.layout.simple_spinner_item, entries); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + context, R.layout.auto_size_spinner_item, entries); + adapter.setDropDownViewResource(R.layout.auto_size_spinner_dropdown_item); mSpinner.setAdapter(adapter); } int textAppearance = a.getResourceId(R.styleable.TextSpinner_textAppearance, android.R.style.TextAppearance_DeviceDefault_Medium); CharSequence title = a.getText(R.styleable.TextSpinner_name); mTitleView.setTextAppearance(context, textAppearance); + mTitleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimensionPixelSize(R.dimen.textsize_16)); setName(title); LayoutParams titleParams = generateDefaultLayoutParams(); - float _16 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, context.getResources().getDisplayMetrics()); - titleParams.setMarginEnd(Math.round(_16)); + titleParams.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.control_dp16)); addViewInLayout(mTitleView, -1, titleParams, true); addViewInLayout(mSpinner, -1, generateDefaultLayoutParams(), true); diff --git a/src/app/src/main/java/com/alipay/hulu/screenRecord/VideoAnalyzer.java b/src/app/src/main/java/com/alipay/hulu/screenRecord/VideoAnalyzer.java index e049760..6503ce2 100644 --- a/src/app/src/main/java/com/alipay/hulu/screenRecord/VideoAnalyzer.java +++ b/src/app/src/main/java/com/alipay/hulu/screenRecord/VideoAnalyzer.java @@ -15,6 +15,7 @@ */ package com.alipay.hulu.screenRecord; +import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.utils.ClassUtil; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.patch.PatchLoadResult; @@ -49,12 +50,12 @@ private VideoAnalyzer() { } - public void doAnalyze(long t1, double exceptDiff,String path, AnalyzeListener listener) { + public void doAnalyze(final long t1, final double exceptDiff, final String path, final AnalyzeListener listener) { this.startTime = System.currentTimeMillis(); this.t1 = t1; this.t2 = 0; - PatchLoadResult patch = ClassUtil.getPatchInfo(SCREEN_RECORD_PATCH); + final PatchLoadResult patch = ClassUtil.getPatchInfo(SCREEN_RECORD_PATCH); if (patch == null) { LogUtil.e("yuawen", "插件screenRecord不存在,无法处理"); @@ -64,39 +65,45 @@ public void doAnalyze(long t1, double exceptDiff,String path, AnalyzeListener li return; } - try { - Class mainClass = patch.classLoader.loadClass(patch.entryClass); - - try { - Method methodWithStart = mainClass.getMethod("compVideoImageWithStart", - String.class, double.class, long.class); - - t2 = ((Double) methodWithStart.invoke(null, path, exceptDiff, t1)).intValue(); - } catch (Exception e) { - LogUtil.e(TAG, "无法找到包含Start的函数", e); - - // 降级到无起始时间的调用 - Method targetMethod = mainClass.getMethod(patch.entryMethod, String.class, double.class); - - t2 = ((Double) targetMethod.invoke(null, path, exceptDiff)).intValue(); - } - - // 解析时间 - long decodeCostTime = (System.currentTimeMillis() - startTime); - - result = t2 - t1; - - LogUtil.i("yuawen", - "path : " + path + - "解析耗时:" + decodeCostTime + " 毫秒\n" + - "\nT1时间为:" + t1 + - "\nT2时间为:" + t2 + - "\n计算耗时为:" + result); - if (listener != null) { - listener.onAnalyzeFinished(result); + // 后台运算 + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + try { + Class mainClass = patch.classLoader.loadClass(patch.entryClass); + + try { + Method methodWithStart = mainClass.getMethod("compVideoImageWithStart", + String.class, double.class, long.class); + + t2 = ((Double) methodWithStart.invoke(null, path, exceptDiff, t1)).intValue(); + } catch (Exception e) { + LogUtil.e(TAG, "无法找到包含Start的函数", e); + + // 降级到无起始时间的调用 + Method targetMethod = mainClass.getMethod(patch.entryMethod, String.class, double.class); + + t2 = ((Double) targetMethod.invoke(null, path, exceptDiff)).intValue(); + } + + // 解析时间 + long decodeCostTime = (System.currentTimeMillis() - startTime); + + result = t2 - t1; + + LogUtil.i("yuawen", + "path : " + path + + "解析耗时:" + decodeCostTime + " 毫秒\n" + + "\nT1时间为:" + t1 + + "\nT2时间为:" + t2 + + "\n计算耗时为:" + result); + if (listener != null) { + listener.onAnalyzeFinished(result); + } + } catch (Exception e) { + LogUtil.e(TAG, "Catch java.lang.Exception: " + e.getMessage(), e); + } } - } catch (Exception e) { - LogUtil.e(TAG, "Catch java.lang.Exception: " + e.getMessage(), e); - } + }); } } diff --git a/src/app/src/main/java/com/alipay/hulu/service/BaseService.java b/src/app/src/main/java/com/alipay/hulu/service/BaseService.java index bd8b91a..3f5b8bc 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/BaseService.java +++ b/src/app/src/main/java/com/alipay/hulu/service/BaseService.java @@ -15,18 +15,45 @@ */ package com.alipay.hulu.service; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Looper; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.utils.ContextUtil; /** * 应用启动的Service,目前只需要FloatWinService来承载 * Created by qiaoruikai on 2019/1/25 3:16 PM. */ public abstract class BaseService extends Service { + private static final String HULU_SERVICE_CHANNEL_ID = "hulu-service"; + protected NotificationManager mNotificationManager; + + @Override + public void startActivity(final Intent intent) { + if (Thread.currentThread() != Looper.getMainLooper().getThread()) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + BaseService.super.startActivity(intent); + } + }); + } else { + super.startActivity(intent); + } + } + @Override public void onCreate() { super.onCreate(); + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);; LauncherApplication.getInstance().notifyCreate(this); } @@ -37,4 +64,30 @@ public void onDestroy() { LauncherApplication.getInstance().notifyDestroy(this); } + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(newBase); + ContextUtil.updateResources(this); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + ContextUtil.updateResources(this); + } + + public Notification.Builder generateNotificationBuilder() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String channelId = HULU_SERVICE_CHANNEL_ID; + NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_DEFAULT); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (nm.getNotificationChannel(channelId) == null) { + nm.createNotificationChannel(channel); + } + return new Notification.Builder(this, channelId); + } else { + return new Notification.Builder(this); + } + } } diff --git a/src/app/src/main/java/com/alipay/hulu/service/CaseRecordManager.java b/src/app/src/main/java/com/alipay/hulu/service/CaseRecordManager.java index dd1abbd..f9cc619 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/CaseRecordManager.java +++ b/src/app/src/main/java/com/alipay/hulu/service/CaseRecordManager.java @@ -15,6 +15,9 @@ */ package com.alipay.hulu.service; +import static com.alipay.hulu.shared.node.action.Constant.TRIGGER_INPUT_METHOD; + +import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -25,20 +28,29 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.Build; +import android.os.Environment; import android.os.IBinder; -import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Pair; +import android.view.Gravity; import android.view.LayoutInflater; +import android.view.MotionEvent; import android.view.View; import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.R; +import com.alipay.hulu.activity.MyApplication; import com.alipay.hulu.activity.NewRecordActivity; import com.alipay.hulu.activity.QRScanActivity; +import com.alipay.hulu.bean.AdvanceCaseSetting; +import com.alipay.hulu.bean.CaseParamBean; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.bean.DeviceInfo; import com.alipay.hulu.common.injector.InjectorService; @@ -49,6 +61,8 @@ import com.alipay.hulu.common.injector.provider.Provider; import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.service.ScreenCaptureService; +import com.alipay.hulu.common.service.TouchService; +import com.alipay.hulu.common.service.base.AppGuardian; import com.alipay.hulu.common.service.base.ExportService; import com.alipay.hulu.common.service.base.LocalService; import com.alipay.hulu.common.tools.BackgroundExecutor; @@ -57,14 +71,14 @@ import com.alipay.hulu.common.utils.DeviceInfoUtil; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; -import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.PermissionUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.event.HandlePermissionEvent; import com.alipay.hulu.event.ScanSuccessEvent; import com.alipay.hulu.shared.event.EventService; import com.alipay.hulu.shared.event.accessibility.AccessibilityServiceImpl; -import com.alipay.hulu.shared.event.bean.UniversalEventBean; +import com.alipay.hulu.shared.event.constant.Constant; +import com.alipay.hulu.shared.event.touch.TouchWrapper; import com.alipay.hulu.shared.io.OperationStepService; import com.alipay.hulu.shared.io.bean.OperationStepMessage; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; @@ -78,15 +92,19 @@ import com.alipay.hulu.shared.node.action.provider.ActionProviderManager; import com.alipay.hulu.shared.node.locater.PositionLocator; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; +import com.alipay.hulu.shared.node.tree.InputWindowTree; import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityNodeProcessor; import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityProvider; +import com.alipay.hulu.shared.node.tree.accessibility.tree.AccessibilityNodeTree; import com.alipay.hulu.shared.node.tree.capture.CaptureTree; -import com.alipay.hulu.shared.node.tree.export.OperationStepProvider; +import com.alipay.hulu.shared.node.tree.export.OperationStepExporter; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; -import com.alipay.hulu.shared.node.utils.AppUtil; import com.alipay.hulu.shared.node.utils.BitmapUtil; +import com.alipay.hulu.shared.node.utils.NodeContext; import com.alipay.hulu.shared.node.utils.PrepareUtil; import com.alipay.hulu.shared.node.utils.RectUtil; +import com.alipay.hulu.shared.scan.ScanCodeType; +import com.alipay.hulu.status.StatusListener; import com.alipay.hulu.tools.HighLightService; import com.alipay.hulu.ui.TwoLevelSelectLayout; import com.alipay.hulu.util.DialogUtils; @@ -96,10 +114,10 @@ import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executors; @@ -107,8 +125,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP; - /** * 操作录制服务 * @@ -123,8 +139,8 @@ public class CaseRecordManager implements ExportService { private Pair localClickPos = null; protected HighLightService highLightService; - private String currentRecordId; - private AtomicInteger operationIdx = new AtomicInteger(1); + protected String currentRecordId; + protected AtomicInteger operationIdx = new AtomicInteger(1); protected boolean isRecording = false; private volatile boolean touchBlockMode = false; @@ -134,25 +150,38 @@ public class CaseRecordManager implements ExportService { // 操作日志输出Handler protected OperationStepService operationStepService; + /** + * 状态监听器 + */ + protected StatusListener statusListener; + + protected volatile boolean displayDialog = false; + /** + * 暂停 + */ + protected volatile boolean pauseFlag = false; + protected volatile boolean nodeLoading = false; protected volatile boolean isExecuting = false; protected volatile boolean forceStopBlocking = false; - private InjectorService injectorService; + protected InjectorService injectorService; protected OperationService operationService; protected EventService eventService; - protected OperationStepProvider stepProvider; + protected OperationStepExporter stepProvider; + + protected volatile OperationContext executingContext; private WindowManager windowManager; - private String app; + protected String app; protected RecordCaseInfo caseInfo; @@ -164,12 +193,25 @@ public class CaseRecordManager implements ExportService { private FloatClickListener listener; private FloatStopListener stopListener; + /** + * 录制前的输入法 + */ + protected String defaultIme; + // 截图服务 private ScreenCaptureService captureService; - private static FloatWinService.OnFloatListener DEFAULT_FLOAT_LISTENER = new FloatWinService.OnFloatListener() { + private FloatWinService.OnFloatListener DEFAULT_FLOAT_LISTENER = new FloatWinService.OnFloatListener() { @Override public void onFloatClick(boolean hide) { + if (isRecording && isExecuting) { + if (executingContext != null) { + executingContext.cancelRunning(); + } + setServiceToTouchBlockMode(); + operationService.invalidRoot(); + notifyDialogDismiss(1000); + } } }; @@ -211,14 +253,16 @@ public void onCreate(Context context) { PermissionUtil.grantHighPrivilegePermission(LauncherApplication.getContext()); currentRecordId = StringUtil.generateRandomString(10); - setServiceToNormalMode(); +// setServiceToNormalMode(); // 启动悬浮窗 connection = new RecordFloatConnection(this); listener = new FloatClickListener(this); stopListener = new FloatStopListener(); - context.bindService(new Intent(context, FloatWinService.class), connection, Context.BIND_AUTO_CREATE); + + // 开始扩展功能处理 + operationService.startExtraActionHandle(); } @@ -226,10 +270,38 @@ public void onCreate(Context context) { public void onScanEvent(final ScanSuccessEvent event) { switch (event.getType()) { case ScanSuccessEvent.SCAN_TYPE_SCHEME: + processAction(new OperationMethod(PerformActionEnum.RESUME), null, binder.loadServiceContext()); + // 向handler发送请求 OperationMethod method = new OperationMethod(PerformActionEnum.JUMP_TO_PAGE); method.putParam(OperationExecutor.SCHEME_KEY, event.getContent()); + // 录制模式需要记录下 + operationAndRecord(method, null); + break; + case ScanSuccessEvent.SCAN_TYPE_QR_CODE: + case ScanSuccessEvent.SCAN_TYPE_BAR_CODE: + processAction(new OperationMethod(PerformActionEnum.RESUME), null, binder.loadServiceContext()); + // 向handler发送请求 + method = new OperationMethod(event.getType() == ScanSuccessEvent.SCAN_TYPE_QR_CODE? + PerformActionEnum.GENERATE_QR_CODE: PerformActionEnum.GENERATE_BAR_CODE); + method.putParam(OperationExecutor.SCHEME_KEY, event.getContent()); + if (event.getType() == ScanSuccessEvent.SCAN_TYPE_BAR_CODE) { + ScanCodeType type = event.getCodeType(); + if (type != null) { + method.putParam(OperationExecutor.GENERATE_CODE_TYPE, type.getCode()); + } + } + + // 录制模式需要记录下 + operationAndRecord(method, null); + break; + case ScanSuccessEvent.SCAN_TYPE_PARAM: + processAction(new OperationMethod(PerformActionEnum.RESUME), null, binder.loadServiceContext()); + // 向handler发送请求 + method = new OperationMethod(PerformActionEnum.LOAD_PARAM); + method.putParam(OperationExecutor.APP_URL_KEY, event.getContent()); + // 录制模式需要记录下 operationAndRecord(method, null); break; @@ -238,6 +310,18 @@ public void onScanEvent(final ScanSuccessEvent event) { } } + @Subscriber(@Param(value = LauncherApplication.SYSTEM_GUARDIAN_EVENT, sticky = false)) + public void onSystemEvent(AppGuardian.ReceiveSystemEvent event) { + if (!isRecording || isExecuting) { + return; + } + if (event == AppGuardian.ReceiveSystemEvent.SCREEN_LOCK && !pauseFlag && !displayDialog) { + processAction(new OperationMethod(PerformActionEnum.PAUSE), null, binder.loadServiceContext()); + } else if (event == AppGuardian.ReceiveSystemEvent.SCREEN_UNLOCK && pauseFlag) { + processAction(new OperationMethod(PerformActionEnum.RESUME), null, binder.loadServiceContext()); + } + } + @Subscriber(@Param(value = CmdTools.FATAL_ADB_CANNOT_RECOVER, sticky = false)) public void notifyAdbClose() { // 先暂停,等ADB恢复 @@ -253,7 +337,7 @@ public void run() { try { boolean result = CmdTools.generateConnection(); if (!result) { - LauncherApplication.getInstance().showToast("ADB未恢复,请连接PC执行'adb tcpip 5555'开启端口"); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.record__adb_hint)); } else { setServiceToTouchBlockMode(); operationService.invalidRoot(); @@ -279,6 +363,13 @@ public void setRecordCase(RecordCaseInfo caseInfo) { return; } + if (statusListener != null) { + JSONObject obj = new JSONObject(); + obj.put("case", caseInfo); + obj.put("time", System.currentTimeMillis()); + statusListener.onStatusChange(StatusListener.STATUS_START, obj); + } + this.caseInfo = caseInfo; // 重置RecordId和operationIdx @@ -298,6 +389,18 @@ public void setRecordCase(RecordCaseInfo caseInfo) { processors.add(AccessibilityNodeProcessor.class); operationService.configProcessors(processors); operationService.configProvider(AccessibilityProvider.class); + operationService.initParams(); + AdvanceCaseSetting setting = JSON.parseObject(caseInfo.getAdvanceSettings(), AdvanceCaseSetting.class); + if (setting != null && setting.getParams() != null) { + Map params = new HashMap<>(setting.getParams().size() + 1); + for (CaseParamBean caseParam: setting.getParams()) { + params.put(caseParam.getParamName(), caseParam.getParamDefaultValue()); + } + + // 设置参数 + operationService.putAllRuntimeParamAtTop(params); + } + operationService.putRuntimeParam(com.alipay.hulu.shared.node.action.Constant.KEY_CURRENT_MODE, "record"); // 查找package信息 PackageInfo pkgInfo = ContextUtil.getPackageInfoByName(LauncherApplication.getContext() @@ -306,15 +409,43 @@ public void setRecordCase(RecordCaseInfo caseInfo) { return; } + final ProgressDialog progressDialog = DialogUtils.showProgressDialog(ContextUtil.getContextThemeWrapper(LauncherApplication.getContext(), R.style.AppDialogTheme), "准备运行环境中"); BackgroundExecutor.execute(new Runnable() { @Override public void run() { - boolean result = PrepareUtil.doPrepareWork(app); - if (result) { - AppUtil.forceStopApp(app); - MiscUtil.sleep(1000); - AppUtil.startApp(app); + try { + PrepareUtil.doPrepareWork(app, new PrepareUtil.PrepareStatus() { + @Override + public void currentStatus(final int progress, final int total, final String message, boolean status) { + if (progressDialog != null && progressDialog.isShowing()) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + progressDialog.setProgress(progress); + progressDialog.setMax(total); + progressDialog.setMessage(message); + } + }); + } + } + }); + } finally { + if (progressDialog != null && progressDialog.isShowing()) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + progressDialog.dismiss(); + } + }); + } + + if (statusListener != null) { + JSONObject obj = new JSONObject(); + obj.put("time", System.currentTimeMillis()); + statusListener.onStatusChange(StatusListener.STATUS_PREPARED, obj); + } } + } }); } @@ -326,12 +457,24 @@ public void startRecord() { isRecording = true; displayDialog = true; - operationService.initParams(); + InjectorService.g().pushMessage(Constant.RUNNING_STATUS, "record"); + + // 先记录下默认输入法 + defaultIme = CmdTools.execHighPrivilegeCmd("settings get secure default_input_method"); + MyApplication.getInstance().updateDefaultIme("com.alipay.hulu/.common.tools.AdbIME"); + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + + // 初始化 operationStepService.startRecord(caseInfo); + TouchService touchService = LauncherApplication.service(TouchService.class); + if (touchService != null) { + touchService.start(); + } + // 刷新数据导出 if (stepProvider == null) { - stepProvider = new OperationStepProvider(currentRecordId); + stepProvider = new OperationStepExporter(currentRecordId); } else { stepProvider.refresh(currentRecordId); } @@ -342,38 +485,200 @@ public void startRecord() { eventService.startTrackTouch(); } + // 重载下当前界面 + operationService.invalidRoot(); + + // 覆盖模式不记录操作位置 + if (SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false)) { + eventService.stopTrackTouch(); + } + // 通知进入触摸屏蔽模式 - setServiceToTouchBlockMode(); + setServiceToTouchBlockModeNoDelay(); // 1秒后再监听 notifyDialogDismiss(1000); + + operationService.invalidRoot(); + + TouchWrapper.getInstance().listen(gestureListener); + TouchWrapper.getInstance().start(); + + // 执行准备操作 + AdvanceCaseSetting setting = JSON.parseObject(caseInfo.getAdvanceSettings(), AdvanceCaseSetting.class); + if (setting != null && setting.getPrepareActions() != null) { + List steps = setting.getPrepareActions(); + for (OperationStep step: steps) { + // 直接操作不录制 + if (step.getOperationNode() == null && step.getOperationMethod() != null) { + operationService.doSomeAction(step.getOperationMethod(), null); + } + } + } } + private View coverView = null; + /** * 进入触摸屏蔽模式 */ protected void setServiceToTouchBlockMode() { - // 延迟500ms + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + setServiceToTouchBlockModeNoDelay(); + } + }, 500); + } + + /** + * 进入触摸屏蔽模式 + */ + protected void setServiceToTouchBlockModeNoDelay() { + if (pauseFlag) { + return; + } LogUtil.d(TAG, "进入触摸阻塞模式"); touchBlockMode = true; - injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_BLOCK); + // 可选CoverMode + if (SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false)) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (coverView == null) { + coverView = LayoutInflater.from(binder.loadServiceContext()).inflate(R.layout.record_cover_view, null); + WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams(); + wmParams.type = com.alipay.hulu.common.constant.Constant.TYPE_ALERT; + wmParams.flags |= 8; + wmParams.gravity = Gravity.LEFT | Gravity.TOP; // 调整悬浮窗口至左上角 + // 以屏幕左上角为原点,设置x、y初始值 + wmParams.x = 0; + wmParams.y = 0; + // 设置悬浮窗口长宽数据 + wmParams.width = WindowManager.LayoutParams.MATCH_PARENT; + wmParams.height = WindowManager.LayoutParams.MATCH_PARENT; + wmParams.format = 1; + wmParams.alpha = 1F; + + coverView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false)) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + TouchWrapper.getInstance().receiveTouchDown(event.getEventTime()); + TouchWrapper.getInstance().receiveTouchPosition(new Point((int) event.getRawX(), (int) event.getRawY()), event.getEventTime()); + } else if (event.getAction() == MotionEvent.ACTION_MOVE) { + TouchWrapper.getInstance().receiveTouchPosition(new Point((int) event.getRawX(), (int) event.getRawY()), event.getEventTime()); + } else if (event.getAction() == MotionEvent.ACTION_UP) { + TouchWrapper.getInstance().receiveTouchPosition(new Point((int) event.getRawX(), (int) event.getRawY()), event.getEventTime()); + TouchWrapper.getInstance().receiveTouchUp(event.getEventTime()); + } + } else { + if (event.getAction() == MotionEvent.ACTION_UP) { + receiveClickPosition(new Point((int) event.getRawX(), (int) event.getRawY())); + } + } + return false; + } + }); + + coverView.setFocusable(false); + + binder.addView(coverView, wmParams); + } + + coverView.setVisibility(View.VISIBLE); + } + }); + } else { + // 延迟500ms + injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_BLOCK); + } } + private long startCallTime = 0; + /** * 进入正常模式 */ protected void setServiceToNormalMode() { touchBlockMode = false; LogUtil.d(TAG, "进入正常触摸模式"); - // 200ms后点击 - injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_NORMAL, 200); + + // 可选CoverMode + if (SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false)) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (coverView != null) { + coverView.setVisibility(View.GONE); + } + } + }, 200); + } else { + // 200ms后点击 + injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_NORMAL, 200); + } + } + + /** + * 进入正常模式 + */ + protected void setServiceToNormalModeNoDelay() { + touchBlockMode = false; + LogUtil.d(TAG, "进入正常触摸模式"); + + // 可选CoverMode + if (SPService.getBoolean(SPService.KEY_RECORD_COVER_MODE, false)) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (coverView != null) { + coverView.setVisibility(View.GONE); + } + } + }); + } else { + // 200ms后点击 + injectorService.pushMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_MODE, AccessibilityServiceImpl.MODE_NORMAL); + } } - @Subscriber(value = @Param(com.alipay.hulu.shared.event.constant.Constant.EVENT_TOUCH_POSITION), thread = RunningThread.BACKGROUND) - public void receiveTouchPosition(UniversalEventBean eventBean) { - Point point = eventBean.getParam(com.alipay.hulu.shared.event.constant.Constant.KEY_TOUCH_POINT); + /** + * 手势监听器 + */ + protected TouchWrapper.GestureListener gestureListener = new TouchWrapper.GestureListener() { + @Override + public void receiveClick(Point point) { + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false)) { + receiveDirectClick(point); + } else { + receiveClickPosition(point); + } + } + + @Override + public void receiveLongClick(Point p, long time) { + receiveClickPosition(p); + } + + @Override + public void receiveScroll(Point start, Point end, long time) { + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false)) { + receiveDirectScroll(start, end, time); + } else { + receiveClickPosition(end); + } + } + }; + + /** + * 收到直接点击 + * @param point + */ + protected void receiveDirectClick(Point point) { if (point == null) { - LogUtil.w(TAG, "收到空触摸消息【%s】", eventBean); + LogUtil.w(TAG, "收到空触摸消息"); return; } @@ -383,22 +688,311 @@ public void receiveTouchPosition(UniversalEventBean eventBean) { return; } - LogUtil.d(TAG, "Receive Touch at time " + eventBean.getTime()); + LogUtil.d(TAG, "Receive Touch at time " + System.currentTimeMillis()); int x = point.x; int y = point.y; // 只针对显示dialog的情况 - if (displayDialog || nodeLoading || isExecuting || !isRecording) { + if (displayDialog || pauseFlag || nodeLoading || isExecuting || !isRecording) { + if (isExecuting && isRecording) { + if (binder.checkInFloat(point)) { + LogUtil.i(TAG, "录制时点到了SoloPi"); + if (executingContext != null) { + executingContext.cancelRunning(); + } + setServiceToTouchBlockMode(); + operationService.invalidRoot(); + notifyDialogDismiss(1000); + } + } return; } LogUtil.i(TAG, "Start notify Touch Event at (%d, %d)", x, y); - // 看下是否点到Soloπ图标 + // 看下是否点到SoloPi图标 if (binder.checkInFloat(point)) { - LogUtil.i(TAG, "点到了Soloπ"); - showFunctionView(null, 2, 3, 4); + LogUtil.i(TAG, "点到了SoloPi"); + startCallTime = System.currentTimeMillis(); + showFunctionView(null); + return; + } + + setServiceToNormalModeNoDelay(); + + nodeLoading = true; + try { + AbstractNodeTree root = operationService.getCurrentRoot(); + + // 如果有显示输入法框,找有input focus的输入框 + NodeContext context = operationService.getNodeContext(); + if (context != null && StringUtil.equals(context.getField(TRIGGER_INPUT_METHOD, ""), "true")) { + AbstractNodeTree node = null; + for (AbstractNodeTree tmp: root) { + if (tmp.getNodeBound().contains(x, y)) { + if (tmp instanceof AccessibilityNodeTree) { + // 找输入框 + if (((AccessibilityNodeTree) tmp).isEditable() && ((AccessibilityNodeTree) tmp).getCurrentNode().isFocused()) { + node = tmp; + break; + } + } + } + } + + + // 如果找到了待输入控件 + if (node != null) { + + // 先切换到默认输入法 + CmdTools.switchToIme(defaultIme); + + displayDialog = true; + + highLightService.highLight(node.getNodeBound(), null); + final AbstractNodeTree finalNode = node; + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + FunctionSelectUtil.showEditView(finalNode, new OperationMethod(PerformActionEnum.INPUT), + binder.loadServiceContext(), new FunctionSelectUtil.FunctionListener() { + @Override + public void onProcessFunction(final OperationMethod method, final AbstractNodeTree node) { + highLightService.removeHightLightSync(); + + // 切换回SoloPi输入法 + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + + // 等悬浮窗消失了再操作 + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + LogUtil.d(TAG, "开始执行操作"); + boolean result = processAction(method, node, binder.loadServiceContext()); + + // 是否需要处理 + if (!result) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(); + } + } + }, 50); + } + + @Override + public void onCancel() { + highLightService.removeHightLightSync(); + + setServiceToTouchBlockModeNoDelay(); + notifyDialogDismiss(); + } + }); + } + }); + return; + } + } + + final AbstractNodeTree node = PositionLocator.findDeepestNode(root, x, y); + LogUtil.i(TAG, "目标节点:%s", node); + + // 节点没拿到 + if (node == null) { + LogUtil.e(TAG, "Get node at (" + x + ", " + y + ") null"); + setServiceToTouchBlockMode(); + return; + } + + if (node instanceof InputWindowTree) { + + AbstractNodeTree targetNode = null; + for (AbstractNodeTree tmp: root) { + if (tmp instanceof AccessibilityNodeTree) { + // 找输入框 + if (((AccessibilityNodeTree) tmp).isEditable() && ((AccessibilityNodeTree) tmp).getCurrentNode().isFocused()) { + targetNode = tmp; + break; + } + } + } + + + // 如果找到了待输入控件 + if (targetNode != null) { + + // 先切换到默认输入法 + CmdTools.switchToIme(defaultIme); + + displayDialog = true; + + highLightService.highLight(targetNode.getNodeBound(), null); + final AbstractNodeTree finalNode = targetNode; + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + FunctionSelectUtil.showEditView(finalNode, new OperationMethod(PerformActionEnum.INPUT), + binder.loadServiceContext(), new FunctionSelectUtil.FunctionListener() { + @Override + public void onProcessFunction(final OperationMethod method, final AbstractNodeTree node) { + highLightService.removeHightLightSync(); + + // 切换回SoloPi输入法 + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + + // 等悬浮窗消失了再操作 + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + LogUtil.d(TAG, "开始执行操作"); + boolean result = processAction(method, node, binder.loadServiceContext()); + + // 是否需要处理 + if (!result) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(); + } + } + }, 50); + } + + @Override + public void onCancel() { + highLightService.removeHightLightSync(); + + setServiceToTouchBlockModeNoDelay(); + notifyDialogDismiss(); + } + }); + } + }); + return; + } else { + LogUtil.w(TAG, "Can't find target node, even input method is display"); + LauncherApplication.getInstance().showToast("未能找到输入控件,无法操作"); + setServiceToTouchBlockMode(); + return; + } + } + + Rect bound = node.getNodeBound(); + float xFactor = (x - bound.left) / (float) bound.width(); + float yFactor = (y - bound.top) / (float) bound.height(); + + final OperationMethod method = new OperationMethod(PerformActionEnum.CLICK); + // 添加控件点击位置 + method.putParam(OperationExecutor.LOCAL_CLICK_POS_KEY, xFactor + "," + yFactor); +// startCallTime = System.currentTimeMillis(); + highLightService.highLight(bound, point); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + highLightService.removeHightLightSync(); + boolean result = CaseRecordManager.this.processAction(method, node, binder.loadServiceContext()); + if (!result) { + CaseRecordManager.this.setServiceToTouchBlockMode(); + CaseRecordManager.this.notifyDialogDismiss(); + } + } + }, 200); + + + } finally { + nodeLoading = false; + } + } + + /** + * 收到直接滑动 + * @param start + * @param end + * @param time + */ + protected void receiveDirectScroll(Point start, Point end, long time) { + if (start == null || end == null) { + LogUtil.w(TAG, "收到空触摸消息"); + return; + } + + // 非触摸阻塞模式 + if (!touchBlockMode) { + LogUtil.d(TAG, "当前非阻塞模式"); + return; + } + + LogUtil.d(TAG, "Receive Touch at time " + System.currentTimeMillis()); + + // 只针对显示dialog的情况 + if (displayDialog || pauseFlag || nodeLoading || isExecuting || !isRecording) { + return; + } + + setServiceToNormalModeNoDelay(); + + LogUtil.i(TAG, "Receive scroll from %s to %s", start, end); + int xDistance = end.x - start.x; + int yDistance = end.y - start.y; + DisplayMetrics dm = new DisplayMetrics(); + ((WindowManager) LauncherApplication.getInstance().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRealMetrics(dm); + int height = dm.heightPixels; + int width = dm.widthPixels; + + OperationMethod method = new OperationMethod(); + method.putParam(OperationExecutor.SCROLL_TIME, Long.toString(time)); + if (Math.abs(xDistance) > Math.abs(yDistance)) { + if (xDistance < 0) { + method.setActionEnum(PerformActionEnum.GLOBAL_SCROLL_TO_RIGHT); + } else { + method.setActionEnum(PerformActionEnum.GLOBAL_SCROLL_TO_LEFT); + } + + method.putParam(OperationExecutor.SCROLL_DISTANCE, Integer.toString((int) (Math.abs(xDistance) / (float) width))); + } else { + if (yDistance < 0) { + method.setActionEnum(PerformActionEnum.GLOBAL_SCROLL_TO_TOP); + } else { + method.setActionEnum(PerformActionEnum.GLOBAL_SCROLL_TO_BOTTOM); + } + method.putParam(OperationExecutor.SCROLL_DISTANCE, Integer.toString((int) (Math.abs(yDistance) / (float) height))); + } + + boolean result = processAction(method, null, binder.loadServiceContext()); + if (!result) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(); + } + } + +// @Subscriber(value = @Param(com.alipay.hulu.shared.event.constant.Constant.EVENT_TOUCH_POSITION), thread = RunningThread.BACKGROUND) + public void receiveClickPosition(Point point) { + if (point == null) { + LogUtil.w(TAG, "收到空触摸消息"); + return; + } + + // 非触摸阻塞模式 + if (!touchBlockMode) { + LogUtil.d(TAG, "当前非阻塞模式"); + return; + } + + LogUtil.d(TAG, "Receive Touch at time " + System.currentTimeMillis()); + + int x = point.x; + int y = point.y; + + // 只针对显示dialog的情况 + if (displayDialog || pauseFlag || nodeLoading || isExecuting || !isRecording) { + return; + } + + LogUtil.i(TAG, "Start notify Touch Event at (%d, %d)", x, y); + + // 看下是否点到SoloPi图标 + if (binder.checkInFloat(point)) { + LogUtil.i(TAG, "点到了SoloPi"); + startCallTime = System.currentTimeMillis(); + showFunctionView(null); return; } @@ -420,7 +1014,8 @@ public void receiveTouchPosition(UniversalEventBean eventBean) { float yFactor = (y - bound.top) / (float) bound.height(); localClickPos = new Pair<>(xFactor, yFactor); - showFunctionView(node, 1); + startCallTime = System.currentTimeMillis(); + showFunctionView(node); } finally { nodeLoading = false; } @@ -432,14 +1027,10 @@ public void receiveTouchPosition(UniversalEventBean eventBean) { * @param method * @param target */ - protected void operationAndRecord(OperationMethod method, AbstractNodeTree target) { + protected boolean operationAndRecord(OperationMethod method, AbstractNodeTree target) { OperationStep step = doAndRecordAction(method, target); if (step == null) { - if (!forceStopBlocking && !displayDialog) { - setServiceToTouchBlockMode(); - } - PerformActionEnum action = method.getActionEnum(); String desc; if (action == PerformActionEnum.OTHER_GLOBAL || action == PerformActionEnum.OTHER_NODE) { @@ -456,14 +1047,15 @@ protected void operationAndRecord(OperationMethod method, AbstractNodeTree targe desc = action.getDesc(); } - LauncherApplication.getInstance().showToast(binder.loadServiceContext(), String.format(Locale.CHINA, "执行操作[%s]失败,请尝试重新执行", desc)); - return; + LauncherApplication.getInstance().showToast(binder.loadServiceContext(), StringUtil.getString(R.string.record__execute_fail, desc)); + return false; } OperationStepMessage message = new OperationStepMessage(); message.setStepIdx(step.getOperationIndex()); message.setGeneralOperationStep(step); - injectorService.pushMessage(com.alipay.hulu.shared.io.constant.Constant.NOTIFY_RECORD_STEP, message, true); + injectorService.pushMessage(OperationStepService.NOTIFY_OPERATION_STEP, message, true); + return true; } /** @@ -477,7 +1069,7 @@ protected OperationStep doAndRecordAction(OperationMethod method, AbstractNodeTr updateFloatIcon(R.drawable.solopi_running); // 如果是控件操作,需要记录操作控件信息 - if (target != null && !(target instanceof CaptureTree) && captureService != null) { + if (target != null && !(target instanceof CaptureTree) && captureService != null && target.getCapture() == null) { DisplayMetrics metrics = new DisplayMetrics(); windowManager.getDefaultDisplay().getRealMetrics(metrics); @@ -497,8 +1089,9 @@ protected OperationStep doAndRecordAction(OperationMethod method, AbstractNodeTr if (capture != null) { // 截取区域 Rect rect = target.getNodeBound(); - Rect scaledRect = RectUtil.safetyScale(rect, radio, capture.getWidth(), - capture.getHeight()); + // 多截取一些区域,防止查找时缺乏信息 + Rect scaledRect = RectUtil.safetyExpend(RectUtil.safetyScale(rect, radio, capture.getWidth(), + capture.getHeight()), 10, capture.getWidth(), capture.getHeight()); Bitmap crop = Bitmap.createBitmap(capture, scaledRect.left, scaledRect.top, scaledRect.width(), @@ -524,12 +1117,24 @@ public void notifyOperationFinish() { isExecuting = false; if (!forceStopBlocking) { setServiceToTouchBlockMode(); + notifyDialogDismiss(100); } updateFloatIcon(R.drawable.solopi_float); } + + @Override + public void onContextReceive(OperationContext context) { + executingContext = context; + } }); } catch (Exception e) { LogUtil.e(TAG, "doRecord action throw : " + e.getMessage(), e); + isExecuting = false; + if (!forceStopBlocking) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(100); + } + updateFloatIcon(R.drawable.solopi_float); } if (step == null) { @@ -544,7 +1149,7 @@ public void notifyOperationFinish() { * 更新悬浮窗图标 * @param res */ - private void updateFloatIcon(final int res) { + public void updateFloatIcon(final int res) { LauncherApplication.getInstance().runOnUiThread(new Runnable() { @Override public void run() { @@ -553,23 +1158,23 @@ public void run() { }); } - protected static final List NODE_KEYS = new ArrayList<>(); + protected static final List NODE_KEYS = new ArrayList<>(); protected static final List NODE_ICONS = new ArrayList<>(); - protected static final Map> NODE_ACTION_MAP = new HashMap<>(); + protected static final Map> NODE_ACTION_MAP = new HashMap<>(); - protected static final List GLOBAL_KEYS = new ArrayList<>(); + protected static final List GLOBAL_KEYS = new ArrayList<>(); protected static final List GLOBAL_ICONS = new ArrayList<>(); - protected static final Map> GLOBAL_ACTION_MAP = new HashMap<>(); + protected static final Map> GLOBAL_ACTION_MAP = new HashMap<>(); // 初始化二级菜单 static { // 节点操作 - NODE_KEYS.add("click"); + NODE_KEYS.add(R.string.function_group__click); NODE_ICONS.add(R.drawable.dialog_action_drawable_quick_click_2); List clickActions = new ArrayList<>(); clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK)); @@ -577,38 +1182,41 @@ public void run() { clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_IF_EXISTS)); clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_QUICK)); clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.MULTI_CLICK)); - NODE_ACTION_MAP.put("click", clickActions); + clickActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLICK_AND_INPUT)); + NODE_ACTION_MAP.put(R.string.function_group__click, clickActions); - NODE_KEYS.add("input"); + NODE_KEYS.add(R.string.function_group__input); NODE_ICONS.add(R.drawable.dialog_action_drawable_input); List inputActions = new ArrayList<>(); inputActions.add(convertPerformActionToSubMenu(PerformActionEnum.INPUT)); inputActions.add(convertPerformActionToSubMenu(PerformActionEnum.INPUT_SEARCH)); - NODE_ACTION_MAP.put("input", inputActions); + NODE_ACTION_MAP.put(R.string.function_group__input, inputActions); - NODE_KEYS.add("scroll"); + NODE_KEYS.add(R.string.function_group__scroll); NODE_ICONS.add(R.drawable.dialog_action_drawable_scroll); List scrollActions = new ArrayList<>(); scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCROLL_TO_BOTTOM)); scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCROLL_TO_TOP)); scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCROLL_TO_LEFT)); scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCROLL_TO_RIGHT)); - NODE_ACTION_MAP.put("scroll", scrollActions); + scrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GESTURE)); + NODE_ACTION_MAP.put(R.string.function_group__scroll, scrollActions); - NODE_KEYS.add("assert"); + NODE_KEYS.add(R.string.function_group__assert); NODE_ICONS.add(R.drawable.dialog_action_drawable_assert); List assertActions = new ArrayList<>(); assertActions.add(convertPerformActionToSubMenu(PerformActionEnum.ASSERT)); assertActions.add(convertPerformActionToSubMenu(PerformActionEnum.SLEEP_UNTIL)); assertActions.add(convertPerformActionToSubMenu(PerformActionEnum.LET_NODE)); - NODE_ACTION_MAP.put("assert", assertActions); + assertActions.add(convertPerformActionToSubMenu(PerformActionEnum.CHECK_NODE)); + NODE_ACTION_MAP.put(R.string.function_group__assert, assertActions); - NODE_KEYS.add("other"); + NODE_KEYS.add(R.string.function_group__extra); NODE_ICONS.add(R.drawable.dialog_action_drawable_extra); // 全局操作 - GLOBAL_KEYS.add("device"); + GLOBAL_KEYS.add(R.string.function_group__device); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_device_operation); List gDeviceActions = new ArrayList<>(); gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.BACK)); @@ -619,44 +1227,60 @@ public void run() { gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.EXECUTE_SHELL)); gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.NOTIFICATION)); gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.RECENT_TASK)); - GLOBAL_ACTION_MAP.put("device", gDeviceActions); + GLOBAL_ACTION_MAP.put(R.string.function_group__device, gDeviceActions); - GLOBAL_KEYS.add("app"); + GLOBAL_KEYS.add(R.string.function_group__app); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_app_operation); List gAppActions = new ArrayList<>(); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.GOTO_INDEX)); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.CHANGE_MODE)); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.JUMP_TO_PAGE)); + gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.GENERATE_QR_CODE)); + gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.GENERATE_BAR_CODE)); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.KILL_PROCESS)); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.CLEAR_DATA)); + gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.ASSERT_TOAST)); gAppActions.add(convertPerformActionToSubMenu(PerformActionEnum.RELOAD)); - GLOBAL_ACTION_MAP.put("app", gAppActions); + GLOBAL_ACTION_MAP.put(R.string.function_group__app, gAppActions); - GLOBAL_KEYS.add("scroll"); + GLOBAL_KEYS.add(R.string.function_group__scroll); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_scroll); List gScrollActions = new ArrayList<>(); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_BOTTOM)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_TOP)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_LEFT)); gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_SCROLL_TO_RIGHT)); - GLOBAL_ACTION_MAP.put("scroll", gScrollActions); - - GLOBAL_KEYS.add("info"); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.KEYBOARD_INPUT)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.INPUT_GLOBAL)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_PINCH_OUT)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_PINCH_IN)); + gScrollActions.add(convertPerformActionToSubMenu(PerformActionEnum.GLOBAL_GESTURE)); + GLOBAL_ACTION_MAP.put(R.string.function_group__scroll, gScrollActions); + + GLOBAL_KEYS.add(R.string.function_group__info); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_device_info); List gInfoActions = new ArrayList<>(); gInfoActions.add(convertPerformActionToSubMenu(PerformActionEnum.DEVICE_INFO)); - GLOBAL_ACTION_MAP.put("info", gInfoActions); + gInfoActions.add(convertPerformActionToSubMenu(PerformActionEnum.LOAD_PARAM)); + GLOBAL_ACTION_MAP.put(R.string.function_group__info, gInfoActions); - GLOBAL_KEYS.add("other"); + GLOBAL_KEYS.add(R.string.function_group__extra); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_extra); + GLOBAL_KEYS.add(R.string.function_group__logic); + GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_logic); + List gLoopActions = new ArrayList<>(); + gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.LET)); + gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.CHECK)); + gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.LOAD_PARAM)); + GLOBAL_ACTION_MAP.put(R.string.function_group__logic, gLoopActions); - GLOBAL_KEYS.add("control"); + GLOBAL_KEYS.add(R.string.function_group__control); GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_finish); List gControlActions = new ArrayList<>(); gControlActions.add(convertPerformActionToSubMenu(PerformActionEnum.FINISH)); gControlActions.add(convertPerformActionToSubMenu(PerformActionEnum.PAUSE)); - GLOBAL_ACTION_MAP.put("control", gControlActions); + GLOBAL_ACTION_MAP.put(R.string.function_group__control, gControlActions); } /** @@ -664,12 +1288,16 @@ public void run() { * * @param node */ - private void showFunctionView(final AbstractNodeTree node, final Integer... levels) { + private synchronized void showFunctionView(final AbstractNodeTree node) { + if (displayDialog || pauseFlag) { + return; + } + // 没有操作 displayDialog = true; - final List keys; + final List keys; final List icons; - final Map> secondLevel = new HashMap<>(); + final Map> secondLevel = new HashMap<>(); if (node != null) { Pair pos = localClickPos; @@ -699,7 +1327,7 @@ public void run() { root.setMinimumHeight(20); - DialogUtils.showCustomView(binder.loadServiceContext(), root, "确定", new Runnable() { + DialogUtils.showCustomView(binder.loadServiceContext(), root, StringUtil.getString(R.string.constant__confirm), new Runnable() { @Override public void run() { Rect scaledRect = crop.getCropRect(); @@ -715,9 +1343,9 @@ public void run() { localClickPos = new Pair<>(0.5F, 0.5F); } - showFunctionView(captureTree, levels); + showFunctionView(captureTree); } - }, "取消", new Runnable() { + }, StringUtil.getString(R.string.constant__cancel), new Runnable() { @Override public void run() { captureTree.resetBound(); @@ -735,7 +1363,7 @@ public void run() { keys = new ArrayList<>(NODE_KEYS); icons = new ArrayList<>(NODE_ICONS); secondLevel.putAll(NODE_ACTION_MAP); - secondLevel.put("other", loadOtherActions(PerformActionEnum.OTHER_NODE, node)); + secondLevel.put(R.string.function_group__extra, loadOtherActions(PerformActionEnum.OTHER_NODE, node)); Rect bound = node.getNodeBound(); @@ -753,7 +1381,7 @@ public void run() { secondLevel.putAll(GLOBAL_ACTION_MAP); // 加入额外操作 - secondLevel.put("other", loadOtherActions(PerformActionEnum.OTHER_GLOBAL, null)); + secondLevel.put(R.string.function_group__extra, loadOtherActions(PerformActionEnum.OTHER_GLOBAL, null)); } setServiceToNormalMode(); @@ -764,6 +1392,8 @@ public void run() { return; } + // 先切换到默认输入法 + CmdTools.switchToIme(defaultIme); // 处理方法 FunctionSelectUtil.showFunctionView(context, node, keys, icons, secondLevel, highLightService, operationService, getLocalClickPos(), new FunctionSelectUtil.FunctionListener() { @@ -771,20 +1401,23 @@ highLightService, operationService, getLocalClickPos(), new FunctionSelectUtil.F public void onProcessFunction(final OperationMethod method, final AbstractNodeTree node) { LogUtil.d(TAG, "悬浮窗消失"); + // 切换回SoloPi输入法 + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); + // 等悬浮窗消失了再操作 - LauncherApplication.getInstance().runOnUiThread(new Runnable() { + BackgroundExecutor.execute(new Runnable() { @Override public void run() { - // 返回是否需要恢复阻塞 + // 返回是否处理完毕 boolean processResult = processAction(method, node, context); // 进行后续处理 - if (processResult) { + if (!processResult) { setServiceToTouchBlockMode(); notifyDialogDismiss(); } } - }); + }, 50); } @Override @@ -794,7 +1427,7 @@ public void onCancel() { ((CaptureTree) node).resetBound(); } - setServiceToTouchBlockMode(); + setServiceToTouchBlockModeNoDelay(); notifyDialogDismiss(); } }); @@ -841,6 +1474,18 @@ private void provideDisplayContent(final FloatWinService.FloatBinder binder) { protected boolean processAction(OperationMethod method, AbstractNodeTree node, final Context context) { PerformActionEnum action = method.getActionEnum(); if (action == PerformActionEnum.FINISH) { + + // 删除临时图片 + File targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + targetDir = new File(targetDir, "solopi"); + if (targetDir.exists()) { + FileUtils.deleteFile(targetDir); + } + + // 切换回默认输入法 + MyApplication.getInstance().updateDefaultIme(defaultIme); + CmdTools.switchToIme(defaultIme); + isRecording = false; displayDialog = false; isExecuting = false; @@ -848,49 +1493,114 @@ protected boolean processAction(OperationMethod method, AbstractNodeTree node, f // 初始化运行环境 operationService.initParams(); - operationStepService.stopRecord(); + TouchService touchService = LauncherApplication.service(TouchService.class); + if (touchService != null) { + touchService.stop(); + } + + boolean processed = operationStepService.stopRecord(context); eventService.stopTrackAccessibilityEvent(); eventService.stopTrackTouch(); + LauncherApplication.getInstance().stopServiceByName(OperationService.class.getName()); setServiceToNormalMode(); - // 恢复悬浮窗 - binder.restoreFloat(); - Intent intent = new Intent(context, NewRecordActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(NewRecordActivity.NEED_REFRESH_PAGE, true); - context.startActivity(intent); + if (statusListener != null) { + JSONObject obj = new JSONObject(); + obj.put("time", System.currentTimeMillis()); + if (caseInfo.getId() > 0) { + obj.put("id", caseInfo.getId()); + } + obj.put("caseName", caseInfo.getCaseName()); + statusListener.onStatusChange(StatusListener.STATUS_STOP, obj); + } - LauncherApplication.getInstance().stopServiceByName(CaseRecordManager.class.getName()); - return false; + if (!processed) { + binder.restoreFloat(); + // 恢复悬浮窗 + binder.restoreFloat(); + Intent intent = new Intent(context, NewRecordActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(NewRecordActivity.NEED_REFRESH_PAGE, true); + context.startActivity(intent); + + LauncherApplication.getInstance().stopServiceByName(CaseRecordManager.class.getName()); + } else { + LauncherApplication.getInstance().stopServiceByName(CaseRecordManager.class.getName()); + } + + TouchWrapper.getInstance().cancelListen(gestureListener); + TouchWrapper.getInstance().stop(); + + // 不能影响其他操作 + if (SPService.getBoolean(SPService.KEY_USE_EASY_MODE, false)) { + SPService.putBoolean(SPService.KEY_USE_EASY_MODE, false); + } + return true; } else if (action == PerformActionEnum.PAUSE) { setServiceToNormalMode(); displayDialog = true; + pauseFlag = true; binder.registerFloatClickListener(new FloatWinService.OnFloatListener() { @Override public void onFloatClick(boolean hide) { + pauseFlag = false; setServiceToTouchBlockMode(); operationService.invalidRoot(); notifyDialogDismiss(1000); binder.registerFloatClickListener(DEFAULT_FLOAT_LISTENER); } }); - return false; - } else if (action == PerformActionEnum.JUMP_TO_PAGE) { + return true; + } else if (action == PerformActionEnum.RESUME) { + pauseFlag = false; + setServiceToTouchBlockMode(); + operationService.invalidRoot(); + notifyDialogDismiss(1000); + binder.registerFloatClickListener(DEFAULT_FLOAT_LISTENER); + return true; + } else if (action == PerformActionEnum.JUMP_TO_PAGE + || action == PerformActionEnum.GENERATE_QR_CODE + || action == PerformActionEnum.GENERATE_BAR_CODE + || action == PerformActionEnum.LOAD_PARAM) { if (!StringUtil.equals(method.getParam("scan"), "1")) { operationAndRecord(method, node); } else { - Intent intent = new Intent(context, QRScanActivity.class); - intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_SCHEME); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(intent); - setServiceToTouchBlockMode(); + if (action == PerformActionEnum.JUMP_TO_PAGE) { + Intent intent = new Intent(context, QRScanActivity.class); + intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_SCHEME); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + processAction(new OperationMethod(PerformActionEnum.PAUSE), null, context); +// setServiceToTouchBlockMode(); + } else if (action == PerformActionEnum.GENERATE_QR_CODE) { + Intent intent = new Intent(context, QRScanActivity.class); + intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_QR_CODE); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + processAction(new OperationMethod(PerformActionEnum.PAUSE), null, context); +// setServiceToTouchBlockMode(); + } else if (action == PerformActionEnum.GENERATE_BAR_CODE) { + Intent intent = new Intent(context, QRScanActivity.class); + intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_BAR_CODE); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + processAction(new OperationMethod(PerformActionEnum.PAUSE), null, context); +// setServiceToTouchBlockMode(); + } else if (action == PerformActionEnum.LOAD_PARAM) { + Intent intent = new Intent(context, QRScanActivity.class); + intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_PARAM); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + processAction(new OperationMethod(PerformActionEnum.PAUSE), null, context); +// setServiceToTouchBlockMode(); + } } } else { - operationAndRecord(method, node); + return operationAndRecord(method, node); } - return true; + return false; } @Subscriber(@Param(value = "FloatClickMethod", sticky = false)) @@ -938,18 +1648,6 @@ public boolean isSupportedDevice() { return true; } - @Subscriber(@Param(com.alipay.hulu.shared.event.constant.Constant.EVENT_ACCESSIBILITY_GESTURE)) - public void onGesture(UniversalEventBean gestureEvent) { - LogUtil.i(TAG, "System Call Gesture Method: " + gestureEvent); - - Integer gestureId; - if (gestureEvent != null && (gestureId = gestureEvent.getParam(com.alipay.hulu.shared.event.constant.Constant.KEY_GESTURE_TYPE)) != null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && gestureId == GESTURE_SWIPE_UP && !displayDialog && !nodeLoading) { - showFunctionView(null, 2, 3, 4); - } - } - } - public void onDestroy(Context context) { LogUtil.i(TAG, "onDestroy"); @@ -961,6 +1659,8 @@ public void onDestroy(Context context) { listener = null; stopListener = null; + operationService.stopExtraActionHandle(); + injectorService.unregister(this); } @@ -990,14 +1690,14 @@ public void onHandlePermissionEvent(HandlePermissionEvent event) { public void receiveDeviceInfoMessage(UIOperationMessage message) { if (message.eventType == UIOperationMessage.TYPE_DEVICE_INFO) { DeviceInfo info = DeviceInfoUtil.generateDeviceInfo(); - showDialog("设备信息", info.toString(), binder.loadServiceContext(), 0); + showDialog(StringUtil.getString(R.string.ui__device_info), info.toString(), binder.loadServiceContext(), 0); } else if (message.eventType == UIOperationMessage.TYPE_DIALOG) { String info = message.getParam("msg"); String title = message.getParam("title"); showDialog(title, info, binder.loadServiceContext(), 0); } else if (message.eventType == UIOperationMessage.TYPE_COUNT_DOWN) { long timeMillis = message.getParam("time"); - showDialog("SLEEP", "等待" + timeMillis + "ms", binder.loadServiceContext(), timeMillis); + showDialog(StringUtil.getString(R.string.ui__sleep), StringUtil.getString(R.string.ui__sleep_time, timeMillis), binder.loadServiceContext(), timeMillis); } else if (message.eventType == UIOperationMessage.TYPE_DISMISS) { // 如果在显示弹窗,就隐藏下 if (dialogRef != null && dialogRef.get() != null && dialogRef.get().isShowing()) { @@ -1018,7 +1718,7 @@ public void receiveDeviceInfoMessage(UIOperationMessage message) { * @param deviceInfo * @param context */ - public void showDialog(String title, String deviceInfo, Context context, long timeout) { + public void showDialog(final String title, String deviceInfo, Context context, long timeout) { if (TextUtils.isEmpty(deviceInfo)) { return; } @@ -1053,16 +1753,18 @@ public void showDialog(String title, String deviceInfo, Context context, long ti final AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) .setTitle(title) .setView(v) - .setPositiveButton("确定", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - setServiceToTouchBlockMode(); + if (!"SLEEP".equals(title)) { + setServiceToTouchBlockMode(); + notifyDialogDismiss(2000); + } forceStopBlocking = false; dialog.dismiss(); - notifyDialogDismiss(2000); } }).create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); dialog.setCancelable(false); dialog.show(); @@ -1071,16 +1773,27 @@ public void onClick(DialogInterface dialog, int which) { dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); + // timeout fix if (timeout > 0) { - long sleepCount = timeout > 500? timeout - 500: timeout; - LauncherApplication.getInstance().runOnUiThread(new Runnable() { - @Override - public void run() { - displayDialog = false; - forceStopBlocking = false; - dialog.dismiss(); - } - }, sleepCount); + if (timeout > 500) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + displayDialog = false; + forceStopBlocking = false; + dialog.dismiss(); + } + }, timeout - 500); + } else { + displayDialog = false; + forceStopBlocking = false; + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + dialog.dismiss(); + } + }, timeout); + } } } catch (Exception e) { LogUtil.e(TAG, "显示设备信息出现异常", e); @@ -1128,6 +1841,10 @@ private void executeDelay(Runnable runnable, long mill) { } } + public void registerStatusListener(StatusListener statusListener) { + this.statusListener = statusListener; + } + @Subscriber(@Param(SubscribeParamEnum.APP)) public void setApp(String app) { this.app = app; @@ -1147,7 +1864,7 @@ public void onServiceConnected(ComponentName name, IBinder service) { managerRef.get().provideDisplayContent(binder); binder.registerRunClickListener(managerRef.get().listener); binder.registerStopClickListener(managerRef.get().stopListener); - binder.registerFloatClickListener(DEFAULT_FLOAT_LISTENER); + binder.registerFloatClickListener(managerRef.get().DEFAULT_FLOAT_LISTENER); } @Override @@ -1190,7 +1907,7 @@ public boolean onStopClick() { * @param actionEnum * @return */ - private static TwoLevelSelectLayout.SubMenuItem convertPerformActionToSubMenu(PerformActionEnum actionEnum) { + protected static TwoLevelSelectLayout.SubMenuItem convertPerformActionToSubMenu(PerformActionEnum actionEnum) { return new TwoLevelSelectLayout.SubMenuItem(actionEnum.getDesc(), actionEnum.getCode(), actionEnum.getIcon()); } diff --git a/src/app/src/main/java/com/alipay/hulu/service/CaseReplayManager.java b/src/app/src/main/java/com/alipay/hulu/service/CaseReplayManager.java index 8ccfbdb..dc8d62d 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/CaseReplayManager.java +++ b/src/app/src/main/java/com/alipay/hulu/service/CaseReplayManager.java @@ -15,6 +15,7 @@ */ package com.alipay.hulu.service; +import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -24,8 +25,10 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.Build; +import android.os.Environment; import android.os.IBinder; -import android.support.v7.app.AlertDialog; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.LayoutInflater; @@ -34,10 +37,10 @@ import android.view.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; -import android.widget.Toast; import com.alipay.hulu.R; import com.alipay.hulu.activity.MyApplication; +import com.alipay.hulu.bean.OperationStepResult; import com.alipay.hulu.bean.ReplayResultBean; import com.alipay.hulu.bean.ReplayStepInfoBean; import com.alipay.hulu.common.application.LauncherApplication; @@ -49,9 +52,9 @@ import com.alipay.hulu.common.injector.provider.Param; import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.service.ScreenCaptureService; +import com.alipay.hulu.common.service.TouchService; import com.alipay.hulu.common.service.base.ExportService; import com.alipay.hulu.common.service.base.LocalService; -import com.alipay.hulu.common.tools.BackgroundExecutor; import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.DeviceInfoUtil; @@ -73,7 +76,7 @@ import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityNodeProcessor; import com.alipay.hulu.shared.node.tree.accessibility.AccessibilityProvider; import com.alipay.hulu.shared.node.tree.capture.CaptureTree; -import com.alipay.hulu.shared.node.tree.export.OperationStepProvider; +import com.alipay.hulu.shared.node.tree.export.OperationStepExporter; import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; import com.alipay.hulu.shared.node.utils.AppUtil; import com.alipay.hulu.shared.node.utils.BitmapUtil; @@ -82,15 +85,21 @@ import com.alipay.hulu.shared.node.utils.PrepareUtil; import com.alipay.hulu.shared.node.utils.RectUtil; import com.alipay.hulu.tools.HighLightService; +import com.alipay.hulu.util.DialogUtils; import java.io.File; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; /** * 操作回放服务 @@ -98,6 +107,7 @@ */ @LocalService public class CaseReplayManager implements ExportService { + public static final String REPLAY_STEP_FINISH_EVENT = "REPLAY_STEP_FINISH_EVENT"; private static final String TAG = "CaseReplayManager"; /** @@ -120,6 +130,11 @@ public class CaseReplayManager implements ExportService { */ private OperationService operationService; + /** + * 操作服务 + */ + private TouchService touchService; + /** * 高亮服务 */ @@ -137,7 +152,32 @@ public class CaseReplayManager implements ExportService { */ private InjectorService injectorService; - private ExecutorService runningExecutor; + private volatile OperationContext runningContext; + + /** + * 用例运行器 + */ + private final ThreadPoolExecutor runningExecutor = new ThreadPoolExecutor(2, 2, 0L, + TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), new ThreadFactory() { + private final AtomicInteger RUNNING_COUNTER = new AtomicInteger(1); + @Override + public Thread newThread(@NonNull Runnable r) { + String name = String.format(Locale.CHINA, "CaseReplayThread-%d", RUNNING_COUNTER.getAndIncrement()); + return new Thread(r, name); + } + }); + + /** + * Daemon 执行器 + */ + private final ScheduledExecutorService daemonExecutor = Executors.newScheduledThreadPool(2, new ThreadFactory() { + private final AtomicInteger DAEMON_COUNTER = new AtomicInteger(1); + @Override + public Thread newThread(@NonNull Runnable r) { + String name = String.format(Locale.CHINA, "CaseReplayThread-%d", DAEMON_COUNTER.getAndIncrement()); + return new Thread(r, name); + } + }); /** * 目标应用 @@ -166,7 +206,7 @@ public int onRunClick() { if (provider.canStart()) { startProcess(); } else { - Toast.makeText(binder.loadServiceContext(), "无法手动启动", Toast.LENGTH_SHORT).show(); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.replay__not_start_by_hand)); } return 0; } @@ -181,19 +221,27 @@ public void onFloatClick(boolean hide) { } }; + private int stepCount = 0; + + private String defaultIme = null; + + private CaseReplayStatus currentStatus = CaseReplayStatus.NONE; + public void onCreate(Context context) { LauncherApplication app = LauncherApplication.getInstance(); operationService = app.findServiceByName(OperationService.class.getName()); injectorService = app.findServiceByName(InjectorService.class.getName()); injectorService.register(this); - runningExecutor = Executors.newSingleThreadExecutor(); eventService = app.findServiceByName(EventService.class.getName()); highLightService = app.findServiceByName(HighLightService.class.getName()); + touchService = app.findServiceByName(TouchService.class.getName()); // 截图服务 captureService = app.findServiceByName(ScreenCaptureService.class.getName()); windowManager = (WindowManager) app.getSystemService(Context.WINDOW_SERVICE); + + operationService.startExtraActionHandle(); } /** @@ -208,15 +256,12 @@ public void start(AbstractStepProvider provider, OnFinishListener finishListener this.provider = provider; this.finishListener = finishListener; - BackgroundExecutor.execute(new Runnable() { - @Override - public void run() { - boolean result = PrepareUtil.doPrepareWork(app); - if (result) { - AppUtil.forceStopApp(app); - } - } - }); + AppUtil.forceStopApp(app); + + // 初始化运行参数 + stepCount = 0; + defaultIme = null; + currentStatus = CaseReplayStatus.BEFORE_PREPARE; List> processors = new ArrayList<>(); processors.add(AccessibilityNodeProcessor.class); @@ -231,6 +276,24 @@ public void run() { watcher = new ContentChangeWatcher(); watcher.start(); eventService.startTrackAccessibilityEvent(); + if (touchService != null) { + touchService.start(); + } + + // 如果是自动启动 + if (provider.canStart() && SPService.getBoolean(SPService.KEY_REPLAY_AUTO_START, false)) { + // 等待初始化完毕 + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + if (binder == null) { + LauncherApplication.getInstance().runOnUiThread(this, 500); + } else { + startProcess(); + } + } + }, 2000); + } } /** @@ -251,6 +314,8 @@ public void onDestroy(Context context) { binder = null; } + operationService.stopExtraActionHandle(); + runningExecutor.shutdownNow(); LauncherApplication.getInstance().stopServiceByName(HighLightService.class.getName()); } @@ -263,7 +328,7 @@ public void startProcess() { binder.hideFloat(); runningFlag = true; - runningExecutor.execute(new Runnable() { + final Runnable runningR = new Runnable() { @Override public void run() { try { @@ -272,87 +337,209 @@ public void run() { LogUtil.e(TAG, "抛出异常" + e.getMessage(), e); } } - }); + }; + // 启动执行器 + runningExecutor.execute(runningR); + + // 守护线程,10s检查一次状态 + daemonExecutor.schedule(new Runnable() { + @Override + public void run() { + // 执行完毕,停止守护线程 + if (currentStatus == CaseReplayStatus.STOP) { + daemonExecutor.shutdownNow(); + return; + } + + // 没执行完毕,并且没有正在处理的线程 + int count = runningExecutor.getActiveCount(); + if (count == 0) { + runningExecutor.execute(runningR); + } + } + }, 10, TimeUnit.SECONDS); } /** - * 具体执行 + * 具体执行(可重入) */ - private void process() { + private synchronized void process() { if (provider == null) { LogUtil.e(TAG, "provider为空"); return; } + if (currentStatus == CaseReplayStatus.NONE) { + LogUtil.w(TAG, "未准备,无法执行"); + return; + } + + if (currentStatus == CaseReplayStatus.BEFORE_PREPARE) { + prepareAction(); + currentStatus = CaseReplayStatus.PREPARED; + } + + if (currentStatus == CaseReplayStatus.PREPARED || currentStatus == CaseReplayStatus.RUNNING) { + InjectorService injectorService = InjectorService.g(); + // 执行各步骤 + while (runningFlag && provider.hasNext()) { + boolean shouldStop = stepAction(injectorService); + if (shouldStop) { + break; + } + } + + currentStatus = CaseReplayStatus.FINISH_RUNNING; + } + + if (currentStatus == CaseReplayStatus.FINISH_RUNNING) { + suffixAction(); + currentStatus = CaseReplayStatus.STOP; + } + } + + /** + * 前置准备操作 + */ + private void prepareAction() { // 准备 provider.prepare(); - // 先记录下默认输入法 - String defaultIme = CmdTools.execHighPrivilegeCmd("settings get secure default_input_method"); - MyApplication.getInstance().updateDefaultIme("com.alipay.hulu/.tools.AdbIME"); - CmdTools.execHighPrivilegeCmd("settings put secure default_input_method com.alipay.hulu/.tools.AdbIME", 0); - - // 执行各步骤 - while (provider.hasNext()) { - OperationStep step = null; - try { - step = provider.provideStep(); - } catch (final Exception e) { - LogUtil.e(TAG, "Provide step throw exception: " + e.getMessage(), e); - // 强制终止 + Context service = LauncherApplication.getInstance().loadRunningService(); + final ProgressDialog progressDialog = DialogUtils.showProgressDialog(ContextUtil.getContextThemeWrapper(service, R.style.AppDialogTheme), "环境准备中"); + PrepareUtil.PrepareStatus prepareStatus = new PrepareUtil.PrepareStatus() { + @Override + public void currentStatus(final int progress, final int total, final String message, boolean status) { LauncherApplication.getInstance().runOnUiThread(new Runnable() { @Override public void run() { - showDialog("解析异常", e.getClass() + ": " + e.getMessage(), binder.loadServiceContext(), 0); + if (progressDialog == null || !progressDialog.isShowing()) { + return; + } + + // 更新progressDialog的状态 + progressDialog.setProgress(progress); + progressDialog.setMax(total); + progressDialog.setMessage(message); } }); - break; } + }; + PrepareUtil.doPrepareWork(app, prepareStatus); - // 说明特殊情况,执行完毕 - if (step == null) { - break; + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + progressDialog.dismiss(); } + }); - LogUtil.d(TAG, "开始执行操作:%s", step); - updateFloatIcon(R.drawable.solopi_running); + // 先记录下默认输入法 + defaultIme = CmdTools.execHighPrivilegeCmd("settings get secure default_input_method"); + MyApplication.getInstance().updateDefaultIme("com.alipay.hulu/.common.tools.AdbIME"); + CmdTools.switchToIme("com.alipay.hulu/.common.tools.AdbIME"); - String result; - try { - result = processOperation(step); - } catch (Exception e) { - LogUtil.e(TAG, "执行操作抛出异常: " + e.getMessage(), e); - result = "执行异常:" + e.getMessage(); - } + // 初始化 + stepCount = 1; + } - if (result != null) { - LogUtil.e(TAG, "执行步骤出现问题:%s", result); + /** + * 单步操作 + * @return 是否执行完毕 + */ + private boolean stepAction(InjectorService injector) { + OperationStep step = null; + try { + step = provider.provideStep(); + } catch (Throwable t) { + LogUtil.e(TAG, "Load Step failed", t); + } + // 说明特殊情况,执行完毕 + if (step == null) { + return true; + } - boolean isError = provider.reportErrorStep(step, result); + LogUtil.i(TAG, "开始执行操作:%s", step); + updateFloatIcon(R.drawable.solopi_running); - if (StringUtil.equals(result, "回放中止")) { - break; - } + String result; + try { + result = processOperation(step); + } catch (Exception e) { + LogUtil.e(TAG, "执行操作抛出异常: " + e.getMessage(), e); + result = "执行异常:" + e.getMessage(); + } - // 如果是error步骤 - if (isError) { - break; + // 是否阻塞执行 + boolean isError = result != null; + if (isError) { + LogUtil.e(TAG, "执行步骤出现问题:%s", result); + isError = provider.reportErrorStep(step, result, new ArrayList()); + } + + // 有需要监听执行结果的监听器 + if (injector.getReferenceCount(REPLAY_STEP_FINISH_EVENT) > 0) { + OperationStepResult replayResult = new OperationStepResult(); + replayResult.method = step.getOperationMethod().getActionEnum().getCode(); + replayResult.error = result; + replayResult.result = !isError; + File captureFile = new File(FileUtils.getSubDir("tmp"), "step_" + stepCount + ".jpg"); + + // 截图信息 + if (captureService != null) { + Bitmap captureResult = capture(captureFile); + if (captureResult != null) { + replayResult.screenCaptureFile = captureFile; } } - // 更新到原始图标 - updateFloatIcon(R.drawable.solopi_float); - MiscUtil.sleep(200); + injector.pushMessage(REPLAY_STEP_FINISH_EVENT, replayResult); } + // 如果是error步骤 + if (StringUtil.equals(result, "回放终止") || isError) { + return true; + } + + // 更新到原始图标 + updateFloatIcon(R.drawable.solopi_float); + + MiscUtil.sleep(200); + return false; + } + + /** + * 后置操作 + */ + private void suffixAction() { watcher.sleepUntilContentDontChange(); + if (touchService != null) { + touchService.stop(); + } + + // 删除临时图片 + File targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); + targetDir = new File(targetDir, "solopi"); + if (targetDir.exists()) { + FileUtils.deleteFile(targetDir); + } // 切换回默认输入法 MyApplication.getInstance().updateDefaultIme(defaultIme); - CmdTools.execHighPrivilegeCmd("settings put secure default_input_method " + defaultIme, 0); + CmdTools.switchToIme(defaultIme); // 汇报结果 final List resultBeans = provider.genReplayResult(); + if (resultBeans != null && resultBeans.size() > 0) { + DeviceInfo deviceInfo = DeviceInfoUtil.generateDeviceInfo(); + for (ReplayResultBean result: resultBeans) { + result.setDeviceInfo(deviceInfo); + result.setPlatform("Android"); + result.setPlatformVersion(deviceInfo.getSystemVersion()); + } + } + + // 先restore再stop binder.restoreFloat(); binder.stopFloat(); @@ -366,6 +553,31 @@ public void run() { public void stopRunning() { this.runningFlag = false; + if (runningContext != null) { + runningContext.cancelRunning(); + } + } + + + /** + * 执行截图 + * @param captureFile + * @return + */ + private Bitmap capture(File captureFile) { + DisplayMetrics metrics = new DisplayMetrics(); + windowManager.getDefaultDisplay().getRealMetrics(metrics); + + int minEdge = Math.min(metrics.widthPixels, metrics.heightPixels); + float radio = SPService.getInt(SPService.KEY_SCREENSHOT_RESOLUTION, 720) / (float) minEdge; + + // 无法放大 + if (radio > 1) { + radio = 1; + } + + return captureService.captureScreen(captureFile, metrics.widthPixels, metrics.heightPixels, + (int) (radio * metrics.widthPixels), (int) (radio * metrics.heightPixels)); } /** @@ -404,10 +616,18 @@ public void notifyOperationFinish() { LogUtil.d(TAG, "当前操作【%s】执行完毕,执行耗时: %dms", method.getActionEnum().getDesc(), System.currentTimeMillis() - startTime); runningFlag.countDown(); } + + @Override + public void onContextReceive(OperationContext context) { + runningContext = context; + } }; // 对于需要操作节点的记录 + AbstractNodeTree node = null; if (operation.getOperationNode() != null) { + // 解析Node数据 + OperationNode origin = new OperationNode(operation.getOperationNode(), operationService); if (operation.getOperationMethod().getActionEnum() != PerformActionEnum.CLICK_QUICK) { watcher.sleepUntilContentDontChange(); } else { @@ -415,20 +635,18 @@ public void notifyOperationFinish() { MiscUtil.sleep(500); } List prepareActions = new ArrayList<>(); - - AbstractNodeTree node = null; if (operation.getOperationMethod().getActionEnum() == PerformActionEnum.CLICK_IF_EXISTS) { - node = OperationUtil.findAbstractNodeWithoutScroll(operation.getOperationNode(), operationService, prepareActions); + node = OperationUtil.findAbstractNodeWithoutScroll(origin, operationService, prepareActions); if (node == null) { - LogUtil.d(TAG, "未查找到节点【%s】,不进行操作", operation.getOperationNode()); + LogUtil.i(TAG, "未查找到节点【%s】,不进行操作", origin); return null; } - } else if (operation.getOperationMethod().getActionEnum() == PerformActionEnum.CHECK_NODE) { - node = OperationUtil.findAbstractNodeWithoutScroll(operation.getOperationNode(), operationService, prepareActions); + } else if (operation.getOperationMethod().getActionEnum() == PerformActionEnum.CHECK_NODE || operation.getOperationMethod().getActionEnum() == PerformActionEnum.CLICK_QUICK) { + node = OperationUtil.findAbstractNodeWithoutScroll(origin, operationService, prepareActions); if (node == null) { - LogUtil.i(TAG, "未查找到节点【%s】,不进行操作", operation.getOperationNode()); + LogUtil.i(TAG, "未查找到节点【%s】,不进行操作", origin); return "节点未查找到"; } } else if (operation.getOperationMethod().getActionEnum() == PerformActionEnum.SLEEP_UNTIL) { @@ -439,7 +657,7 @@ public void notifyOperationFinish() { long start = System.currentTimeMillis(); while ((System.currentTimeMillis() - start) < time) { - node = OperationUtil.scrollToScreen(operation.getOperationNode(), operationService); + node = OperationUtil.scrollToScreen(origin, operationService); // 如果找到了,直接break if (node != null) { @@ -453,7 +671,7 @@ public void notifyOperationFinish() { // 没找到 if (node == null) { - LogUtil.w(TAG, "未查找到节点【%s】", operation.getOperationNode()); + LogUtil.w(TAG, "未查找到节点【%s】", origin); return "节点未查找到"; } } catch (NumberFormatException e) { @@ -461,9 +679,9 @@ public void notifyOperationFinish() { return "参数错误"; } } else { - node = OperationUtil.findAbstractNode(operation.getOperationNode(), operationService, prepareActions); + node = OperationUtil.findAbstractNode(origin, operationService, prepareActions); if (node == null) { - LogUtil.w(TAG, "未查找到节点【%s】,无法进行操作", operation.getOperationNode()); + LogUtil.w(TAG, "未查找到节点【%s】,无法进行操作", origin); return "节点未查找到"; } } @@ -495,7 +713,7 @@ public void notifyOperationFinish() { capture.getHeight()); Bitmap crop = Bitmap.createBitmap(capture, scaledRect.left, - scaledRect.top, scaledRect.width(), + scaledRect.top, scaledRect.width(), scaledRect.height()); String content = BitmapUtil.bitmapToBase64(crop); @@ -515,49 +733,51 @@ public void notifyOperationFinish() { // 高亮下 highLightAndRemove(node, operation.getOperationMethod()); } + } else { + // 前一次操作时间有记录,需要Sleep这段时间 + watcher.sleepUntilContentDontChange(); + } - // 执行操作 - boolean result = operationService.doSomeAction(operation.getOperationMethod(), node, listener); - if (!result) { - return "执行失败"; - } - - OperationNode opNode = OperationStepProvider.exportNodeToOperationNode(node); - - // 等待操作结束 - try { - runningFlag.await(600 * 100, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); - } - // 输入操作会较耗时,需要等待下 - if (method.getActionEnum() == PerformActionEnum.INPUT || method.getActionEnum() == PerformActionEnum.INPUT_SEARCH) { - MiscUtil.sleep(3000); - watcher.sleepUntilContentDontChange(); - } + // 执行操作 + boolean result = operationService.doSomeAction(operation.getOperationMethod(), node, listener); + if (!result) { + return "执行失败"; + } - stepInfoBean.setFindNode(opNode); + OperationNode opNode = null; + if (node != null) { + opNode = OperationStepExporter.exportNodeToOperationNode(node); + } - provider.onStepInfo(stepInfoBean); + // 等待操作结束 + long sleepTime; + // 成功执行,需要等待10分钟 + // 等待操作结束 + if (node != null) { + sleepTime = 60; + } else if (operation.getOperationMethod().getActionEnum() != PerformActionEnum.SLEEP) { + sleepTime = 600; } else { - // 前一次操作时间有记录,需要Sleep这段时间 + sleepTime = 60 * 60; + } + try { + runningFlag.await(sleepTime, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); + } + + // 输入操作会较耗时,需要等待下 + if (method.getActionEnum() == PerformActionEnum.INPUT + || method.getActionEnum() == PerformActionEnum.INPUT_SEARCH + || method.getActionEnum() == PerformActionEnum.CLICK_AND_INPUT) { + MiscUtil.sleep(1000); watcher.sleepUntilContentDontChange(); + } - // 对于全局操作,直接执行 - boolean result = operationService.doSomeAction(method, null, listener); - if (!result) { - return "执行失败"; - } + stepInfoBean.setFindNode(opNode); - // 成功执行,需要等待 - // 等待操作结束 - try { - runningFlag.await(600 * 100, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - LogUtil.e(TAG, "Catch java.lang.InterruptedException: " + e.getMessage(), e); - } - } + provider.onStepInfo(stepInfoBean); LogUtil.d(TAG, "操作执行完毕"); return null; } @@ -619,14 +839,14 @@ public void setApp(String app) { public void receiveDeviceInfoMessage(UIOperationMessage message) { if (message.eventType == UIOperationMessage.TYPE_DEVICE_INFO) { DeviceInfo info = DeviceInfoUtil.generateDeviceInfo(); - showDialog("设备信息", info.toString(), binder.loadServiceContext(), 0); + showDialog(StringUtil.getString(R.string.ui__device_info), info.toString(), binder.loadServiceContext(), 0); } else if (message.eventType == UIOperationMessage.TYPE_DIALOG) { String info = message.getParam("msg"); String title = message.getParam("title"); showDialog(title, info, binder.loadServiceContext(), 0); } else if (message.eventType == UIOperationMessage.TYPE_COUNT_DOWN) { long timeMillis = message.getParam("time"); - showDialog("SLEEP", "等待" + timeMillis + "ms", binder.loadServiceContext(), timeMillis); + showDialog(StringUtil.getString(R.string.ui__sleep), StringUtil.getString(R.string.ui__sleep_time, timeMillis), binder.loadServiceContext(), timeMillis); } else if (message.eventType == UIOperationMessage.TYPE_DISMISS) { // 隐藏掉原来的Dialog if (dialogRef != null && dialogRef.get() != null && dialogRef.get().isShowing()) { @@ -669,13 +889,13 @@ public void showDialog(String title, String deviceInfo, Context context, long ti final AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) .setTitle(title) .setView(v) - .setPositiveButton("确定", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }).create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); dialog.setCancelable(false); dialog.show(); @@ -741,4 +961,16 @@ public void onServiceDisconnected(ComponentName name) { public interface OnFinishListener { void onFinish(List resultBeans, Context context); } + + /** + * 运行状态 + */ + private enum CaseReplayStatus { + NONE, + BEFORE_PREPARE, + PREPARED, + RUNNING, + FINISH_RUNNING, + STOP, + } } diff --git a/src/app/src/main/java/com/alipay/hulu/service/DisplayManager.java b/src/app/src/main/java/com/alipay/hulu/service/DisplayManager.java index 8c368ad..4e22e77 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/DisplayManager.java +++ b/src/app/src/main/java/com/alipay/hulu/service/DisplayManager.java @@ -20,8 +20,8 @@ import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; @@ -29,6 +29,7 @@ import android.widget.LinearLayout; import com.alipay.hulu.R; +import com.alipay.hulu.adapter.FloatStressAdapter; import com.alipay.hulu.adapter.FloatWinAdapter; import com.alipay.hulu.common.application.LauncherApplication; import com.alipay.hulu.common.injector.InjectorService; @@ -36,7 +37,6 @@ import com.alipay.hulu.common.injector.provider.Provider; import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.tools.BackgroundExecutor; -import com.alipay.hulu.common.utils.ContextUtil; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.display.DisplayItemInfo; @@ -72,6 +72,8 @@ public class DisplayManager { private FloatWinAdapter floatWinAdapter; + private FloatStressAdapter floatStressAdapter; + private int runningMode; private volatile boolean runningFlag = true; @@ -91,6 +93,10 @@ public boolean onStopClick() { private RecyclerView floatWinList; + private RecyclerView floatStressList; + + private View floatStressHide; + private DisplayConnection connection; private static DisplayManager instance; @@ -186,7 +192,7 @@ public synchronized List updateRecordingItems(List failed = new ArrayList<>(); if (newItems != null && newItems.size() > 0) { for (DisplayItemInfo info : newItems) { - boolean result = provider.startDisplay(info.getName()); + boolean result = provider.startDisplay(info.getKey()); // 失败项将取消 if (result) { @@ -255,7 +261,7 @@ private void stopRecord() { final Map> result = provider.stopRecording(); binder.provideDisplayView(provideMainView(binder.loadServiceContext()), - new LinearLayout.LayoutParams(ContextUtil.dip2px(binder.loadServiceContext(), 280), + new LinearLayout.LayoutParams(binder.loadServiceContext().getResources().getDimensionPixelSize(R.dimen.control_float_title_width), ViewGroup.LayoutParams.WRAP_CONTENT)); final String uploadUrl = SPService.getString(SPService.KEY_PERFORMANCE_UPLOAD, null); @@ -267,10 +273,10 @@ public void run() { File folder = RecordUtil.saveToFile(result); // 显示提示框 - LauncherApplication.getInstance().showDialog(binder.loadServiceContext(), "录制数据已经保存到\"" + folder.getPath() + "\"下" , "确定", null); + LauncherApplication.getInstance().showDialog(binder.loadServiceContext(), StringUtil.getString(R.string.performance__record_save, folder.getPath()) , StringUtil.getString(R.string.constant__confirm), null); } else { String response = RecordUtil.uploadData(uploadUrl, result); - LauncherApplication.getInstance().showDialog(binder.loadServiceContext(), "录制数据已经上传至\"" + uploadUrl + "\",响应结果: " + response , "确定", null); + LauncherApplication.getInstance().showDialog(binder.loadServiceContext(), StringUtil.getString(R.string.performance__record_upload, uploadUrl, response), StringUtil.getString(R.string.constant__confirm), null); } } }); @@ -285,8 +291,8 @@ private View provideMainView(Context context) { if (runningMode == DisplayProvider.RECORDING_MODE) { return null; } - - floatWinList = (RecyclerView) LayoutInflater.from(context).inflate(R.layout.display_main_layout, null); + View root = LayoutInflater.from(context).inflate(R.layout.display_main_layout, null); + floatWinList = root.findViewById(R.id.float_recycler_view); floatWinList.setLayoutManager(new LinearLayoutManager(context)); floatWinList.setOnTouchListener(new View.OnTouchListener() { @Override @@ -301,7 +307,37 @@ public boolean onTouch(View v, MotionEvent event) { floatWinList.addItemDecoration(new RecycleViewDivider(context, LinearLayoutManager.HORIZONTAL, 1, context.getResources().getColor(R.color.divider_color))); - return floatWinList; + floatStressList = root.findViewById(R.id.float_stress_recycler_view); + + floatStressList.setLayoutManager(new LinearLayoutManager(context)); + floatStressList.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return false; + } + }); + + floatStressAdapter = new FloatStressAdapter(context); + floatStressList.setAdapter(floatStressAdapter); + // 添加分割线 + floatStressList.addItemDecoration(new RecycleViewDivider(context, + LinearLayoutManager.HORIZONTAL, 1, context.getResources().getColor(R.color.divider_color))); + + floatStressHide = root.findViewById(R.id.float_stress_hide); + floatStressHide.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (floatStressList.getVisibility() == View.VISIBLE) { + floatStressHide.setRotation(0); + floatStressList.setVisibility(View.GONE); + } else { + floatStressHide.setRotation(180); + floatStressList.setVisibility(View.VISIBLE); + } + } + }); + + return root; } private View provideExpendView(Context context) { @@ -330,7 +366,7 @@ public void onServiceConnected(ComponentName name, IBinder service) { // 提供主界面 binder.provideDisplayView(manager.provideMainView(context), - new LinearLayout.LayoutParams(ContextUtil.dip2px(context, 280), + new LinearLayout.LayoutParams(context.getResources().getDimensionPixelSize(R.dimen.control_float_title_width), ViewGroup.LayoutParams.WRAP_CONTENT)); // 提供扩展界面 diff --git a/src/app/src/main/java/com/alipay/hulu/service/FloatWinService.java b/src/app/src/main/java/com/alipay/hulu/service/FloatWinService.java index aa45402..a557231 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/FloatWinService.java +++ b/src/app/src/main/java/com/alipay/hulu/service/FloatWinService.java @@ -27,12 +27,13 @@ import android.graphics.Point; import android.graphics.Rect; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.PowerManager; import android.os.PowerManager.WakeLock; -import android.support.v7.app.AlertDialog; +import android.util.DisplayMetrics; import android.view.Display; import android.view.Gravity; import android.view.LayoutInflater; @@ -49,6 +50,7 @@ import com.alipay.hulu.activity.IndexActivity; import com.alipay.hulu.activity.MyApplication; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.constant.Constant; import com.alipay.hulu.common.injector.InjectorService; import com.alipay.hulu.common.injector.param.RunningThread; import com.alipay.hulu.common.injector.param.SubscribeParamEnum; @@ -68,6 +70,8 @@ import java.util.List; import java.util.Locale; +import androidx.appcompat.app.AlertDialog; + import static android.view.Surface.ROTATION_0; @@ -143,6 +147,8 @@ public class FloatWinService extends BaseService { private OnStopListener stopListener = null; + private OnHomeListener homeListener = null; + private int recordCount = 0; private boolean isCountTime = false; @@ -171,6 +177,10 @@ public class FloatWinService extends BaseService { private String appPackage = ""; private String appName = ""; + static { + LauncherApplication.getInstance().registerSelfAsForegroundService(FloatWinService.class); + } + @Subscriber(@Param(SubscribeParamEnum.APP)) public void setAppPackage(String appPackage){ this.appPackage = appPackage; @@ -189,7 +199,7 @@ public void run() { } } - @Subscriber(@Param(LauncherApplication.SCREEN_ORIENTATION)) + @Subscriber(@Param(Constant.SCREEN_ORIENTATION)) public void setScreenOrientation(int orientation) { if (orientation != currentOrientation) { currentOrientation = orientation; @@ -216,7 +226,7 @@ public void startDialog(String message) { messageText = (TextView) v.findViewById(R.id.loading_dialog_text); loadingDialog = new AlertDialog.Builder(this, R.style.AppDialogTheme) .setView(v) - .setNegativeButton("隐藏", new DialogInterface.OnClickListener() { + .setNegativeButton(R.string.float__hide, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); @@ -224,7 +234,7 @@ public void onClick(DialogInterface dialog, int which) { }) .create(); // 设置dialog - loadingDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + loadingDialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); loadingDialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 loadingDialog.setCancelable(false); } @@ -249,13 +259,16 @@ public void onCreate() { super.onCreate(); LogUtil.d(TAG, "onCreate"); + Notification notification = generateNotificationBuilder().setContentText(getString(R.string.float__toast_title)).setSmallIcon(R.drawable.solopi_main).build(); + startForeground(NOTIFICATION_ID, notification); + handler = new TimeProcessHandler(this); mInjectorService = LauncherApplication.getInstance().findServiceByName(InjectorService.class.getName()); mInjectorService.register(this); if (provider == null) { - provider = new AppInfoProvider(); + provider = AppInfoProvider.getInstance(); mInjectorService.register(provider); } @@ -282,7 +295,7 @@ public void writeFileData(String fileName, String message) { * 初始化界面 */ private void createView() { - view = LayoutInflater.from(this).inflate(R.layout.float_win, null); + view = LayoutInflater.from(ContextUtil.getContextThemeWrapper(this, R.style.AppTheme)).inflate(R.layout.float_win, null); // 关闭按钮 close = (ImageView) view.findViewById(R.id.closeIcon); // 录制开关 @@ -364,7 +377,7 @@ public void onClick(View v) { wm = (WindowManager) getApplicationContext().getSystemService(WINDOW_SERVICE); // 设置LayoutParams(全局变量)相关参数 wmParams = ((MyApplication) getApplication()).getFloatWinParams(); - wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; + wmParams.type = com.alipay.hulu.common.constant.Constant.TYPE_ALERT; wmParams.flags |= 8; wmParams.gravity = Gravity.LEFT | Gravity.TOP; // 调整悬浮窗口至左上角 // 以屏幕左上角为原点,设置x、y初始值 @@ -446,8 +459,9 @@ public void onClick(View v) { homeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - goToHomePage(); - + if (homeListener == null || !homeListener.onHomeClick()) { + goToHomePage(); + } } }); @@ -528,22 +542,28 @@ private void updateCurrentAppName(String name) { */ private void updateViewPosition() { // 更新浮动窗口位置参数 - wmParams.x = (int) (x - mTouchStartX); - wmParams.y = (int) (y - mTouchStartY); - wmParams.alpha = 1F; - wm.updateViewLayout(view, wmParams); + try { + wmParams.x = (int) (x - mTouchStartX); + wmParams.y = (int) (y - mTouchStartY); + wmParams.alpha = 1F; + wm.updateViewLayout(view, wmParams); + } catch (Throwable t) { + LogUtil.e(TAG, "Fail update View layout", t); + } } @Override public int onStartCommand(Intent intent, int flags, int startId) { LogUtil.d(TAG, "onStart"); - Notification notification = new Notification.Builder(this).setContentText("Soloπ悬浮窗正在运行").setSmallIcon(R.drawable.solopi_main).build(); - startForeground(NOTIFICATION_ID, notification); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { + super.onDestroy(); + stopForeground(true); + mNotificationManager.cancel(NOTIFICATION_ID); + // 清理定时任务 mInjectorService.unregister(this.provider); this.provider = null; @@ -561,7 +581,6 @@ public void onDestroy() { editor.putString("state", "stop"); editor.apply(); // 取消注册广播 - super.onDestroy(); } @@ -598,7 +617,6 @@ public Context loadServiceContext() { /** * 提供主窗体 - * * @param baseView * @param params */ @@ -628,9 +646,38 @@ public void run() { }); } + /** + * 设置录制按钮图标 + * @param icon + */ + public void updateRunImage(int icon) { + if (floatWinServiceRef.get() == null) { + return; + } + + FloatWinService service = floatWinServiceRef.get(); + if (icon != 0) { + service.record.setImageResource(icon); + if (icon == RECORDING_ICON) { + service.recordCount = 0; + service.isCountTime = true; + service.recordTime.setVisibility(View.VISIBLE); + service.handler.sendEmptyMessageDelayed(UPDATE_RECORD_TIME, 1000); + } else if (icon == PLAY_ICON) { + service.recordCount = 0; + service.isCountTime = false; + service.recordTime.setVisibility(View.INVISIBLE); + } + } else { + if (service.isCountTime) { + service.recordTime.setVisibility(View.INVISIBLE); + service.isCountTime = false; + } + } + } + /** * 提供扩展窗体 - * * @param expendView * @param params */ @@ -734,6 +781,11 @@ public void registerStopClickListener(OnStopListener listener) { service.stopListener = listener; } + public void registerHomeClickListener(OnHomeListener listener) { + FloatWinService service = floatWinServiceRef.get(); + service.homeListener = listener; + } + /** * 隐藏悬浮窗 */ @@ -779,9 +831,50 @@ public void run() { public void updateFloatIcon(int res) { final FloatWinService service = floatWinServiceRef.get(); service.backgroundIcon.setImageResource(res); - service.cardIcon.setImageResource(res); } + /** + * 开始计时 + */ + public void startTimeRecord() { + final FloatWinService service = floatWinServiceRef.get(); + + // 重置计时 + service.recordCount = 0; + if (!service.isCountTime) { + service.isCountTime = true; + service.recordTime.setVisibility(View.VISIBLE); + service.handler.sendEmptyMessageDelayed(UPDATE_RECORD_TIME, 1000); + } + } + + /** + * 停止计时 + */ + public void stopRecordTime() { + final FloatWinService service = floatWinServiceRef.get(); + + if (service.isCountTime) { + service.isCountTime = false; + service.recordTime.setVisibility(View.GONE); + } + } + + /** + * 更新文字 + * @param text + */ + public void updateText(String text) { + final FloatWinService service = floatWinServiceRef.get(); + + if (!StringUtil.isEmpty(text)) { + service.recordTime.setVisibility(View.VISIBLE); + service.recordTime.setText(text); + } else { + service.recordTime.setVisibility(View.GONE); + service.recordTime.setText(""); + } + } /** * 检查点是否在悬浮窗内 @@ -794,7 +887,7 @@ public boolean checkInFloat(Point point) { return false; } - // 看下是否点到Soloπ图标 + // 看下是否点到SoloPi图标 FloatWinService service = floatWinServiceRef.get(); Rect rect = new Rect(); service.view.getDrawingRect(rect); @@ -804,6 +897,19 @@ public boolean checkInFloat(Point point) { service.view.getWindowVisibleDisplayFrame(r); WindowManager.LayoutParams params = (WindowManager.LayoutParams) service.view.getLayoutParams(); + // Android 10 尺寸获取问题 + if (Build.VERSION.SDK_INT >= 29) { + DisplayMetrics metrics = new DisplayMetrics(); + service.wm.getDefaultDisplay().getRealMetrics(metrics); + r.right = metrics.widthPixels; + Point smallP = new Point(); + service.view.getDisplay().getCurrentSizeRange(smallP, new Point()); + int decoSize = metrics.heightPixels - smallP.y; + if (r.top > decoSize) { + r.top = decoSize; + } + } + int x = r.left + params.x; int y = r.top + params.y; @@ -827,12 +933,12 @@ public boolean checkInFloat(Point point) { private void hideFloatWin() { cardView.setVisibility(View.GONE); Display screenDisplay = ((WindowManager)FloatWinService.this.getSystemService(WINDOW_SERVICE)).getDefaultDisplay(); - Point size = new Point(); - screenDisplay.getSize(size); - x = size.x; + DisplayMetrics metrics = new DisplayMetrics(); + screenDisplay.getRealMetrics(metrics); + x = metrics.widthPixels; //y = (size.y - statusBarHeight) / 2; - y = size.y / 2 - 4 * statusBarHeight; + y = metrics.heightPixels / 2 - 4 * statusBarHeight; updateViewPosition(); // handler.removeCallbacks(task); backgroundIcon.setVisibility(View.VISIBLE); @@ -858,6 +964,10 @@ public interface OnStopListener { boolean onStopClick(); } + public interface OnHomeListener { + boolean onHomeClick(); + } + private static final class TimeProcessHandler extends Handler { private WeakReference serviceRef; @@ -876,6 +986,9 @@ public void handleMessage(Message msg) { switch (msg.what) { case UPDATE_RECORD_TIME: // 每秒钟增加recordCount,作为已录制的时间 + if (!service.isCountTime) { + return; + } service.recordCount++; service.recordTime.setText(timefyCount(service.recordCount)); diff --git a/src/app/src/main/java/com/alipay/hulu/service/InstallReceiver.java b/src/app/src/main/java/com/alipay/hulu/service/InstallReceiver.java index b8d4590..2446b77 100644 --- a/src/app/src/main/java/com/alipay/hulu/service/InstallReceiver.java +++ b/src/app/src/main/java/com/alipay/hulu/service/InstallReceiver.java @@ -32,8 +32,11 @@ public class InstallReceiver extends BroadcastReceiver { private static final String TAG = "InstallReceiver"; @Override public void onReceive(Context context, Intent intent) { + if (intent == null) { + return; + } - if (intent.getAction().equals("android.intent.action.PACKAGE_ADDED")) { // install + if ("android.intent.action.PACKAGE_ADDED".equals(intent.getAction())) { // install String packageName = intent.getDataString(); LogUtil.i(TAG, "安装了 :" + StringUtil.hide(packageName)); @@ -42,7 +45,7 @@ public void onReceive(Context context, Intent intent) { MyApplication.getInstance().notifyAppChangeEvent(); } - if (intent.getAction().equals("android.intent.action.PACKAGE_REMOVED")) { // uninstall + if ("android.intent.action.PACKAGE_REMOVED".equals(intent.getAction())) { // uninstall String packageName = intent.getDataString(); LogUtil.i(TAG, "卸载了 :" + StringUtil.hide(packageName)); diff --git a/src/app/src/main/java/com/alipay/hulu/status/StatusListener.java b/src/app/src/main/java/com/alipay/hulu/status/StatusListener.java new file mode 100644 index 0000000..c8fe33b --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/status/StatusListener.java @@ -0,0 +1,36 @@ +package com.alipay.hulu.status; + +import com.alibaba.fastjson.JSONObject; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import androidx.annotation.StringDef; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public interface StatusListener { + public static final String STATUS_START = "START"; + public static final String STATUS_PREPARED = "PREPARED"; + public static final String STATUS_STOP = "STOP"; + public static final String STATUS_STEP = "STEP"; + + @StringDef({ + STATUS_START, + STATUS_PREPARED, + STATUS_STEP, + STATUS_STOP + }) + @Retention(SOURCE) + @Target({PARAMETER}) + @interface StateDefine{}; + + + /** + * 通知状态变化 + * @param state + * @param extra + */ + void onStatusChange(@StateDefine String state, JSONObject extra); +} diff --git a/src/app/src/main/java/com/alipay/hulu/status/impl/HttpStatusListener.java b/src/app/src/main/java/com/alipay/hulu/status/impl/HttpStatusListener.java new file mode 100644 index 0000000..f03570c --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/status/impl/HttpStatusListener.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.status.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alipay.hulu.common.utils.HttpUtil; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.status.StatusListener; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.RequestBody; + +/** + * 基于HTTP的状态上报器 + */ +public class HttpStatusListener implements StatusListener { + private static final String TAG = HttpStatusListener.class.getSimpleName(); + private String reportUrl; + + private JSONObject reportExtra; + + private StatusListener wrapper; + + public HttpStatusListener(String reportUrl, JSONObject reportExtra) { + this.reportUrl = reportUrl; + this.reportExtra = new JSONObject(); + if (reportExtra != null) { + this.reportExtra.putAll(reportExtra); + } + } + + @Override + public void onStatusChange(String state, JSONObject extra) { + if (wrapper != null) { + wrapper.onStatusChange(state, extra); + } + JSONObject toReport = new JSONObject(reportExtra); + toReport.put("type", state); + toReport.put("value", extra); + + LogUtil.i(TAG, "Prepare to report status %s to url %s", state, reportUrl); + + HttpUtil.post(reportUrl, RequestBody.create(MediaType.get("application/json"), + JSON.toJSONBytes(toReport)), new HttpUtil.Callback(String.class) { + @Override + public void onFailure(Call call, IOException e) { + LogUtil.e(TAG, "Report status failed, throw exception", e); + } + + @Override + public void onResponse(Call call, String result) throws IOException { + LogUtil.i(TAG, "Report status finished, reponse:" + result); + } + }); + } + + public void setWrapper(StatusListener wrapper) { + this.wrapper = wrapper; + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/tools/AdbIME.java b/src/app/src/main/java/com/alipay/hulu/tools/AdbIME.java deleted file mode 100644 index e7aa443..0000000 --- a/src/app/src/main/java/com/alipay/hulu/tools/AdbIME.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright (C) 2015-present, Ant Financial Services Group - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.alipay.hulu.tools; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.inputmethodservice.InputMethodService; -import android.view.KeyEvent; -import android.view.View; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputConnection; -import android.view.inputmethod.InputMethodManager; - -import com.alipay.hulu.R; -import com.alipay.hulu.activity.MyApplication; -import com.alipay.hulu.common.application.LauncherApplication; -import com.alipay.hulu.common.tools.BackgroundExecutor; -import com.alipay.hulu.common.tools.CmdTools; -import com.alipay.hulu.common.utils.MiscUtil; -import com.alipay.hulu.common.utils.StringUtil; -import com.alipay.hulu.shared.node.OperationService; -import com.alipay.hulu.shared.node.action.OperationMethod; -import com.alipay.hulu.shared.node.action.PerformActionEnum; - -/** - * Created by lezhou.wyl on 2018/2/8. - */ -public class AdbIME extends InputMethodService { - private static final String TAG = "AdbIME"; - - private String IME_MESSAGE = "ADB_INPUT_TEXT"; - private String IME_SEARCH_MESSAGE = "ADB_SEARCH_TEXT"; - private String IME_CHARS = "ADB_INPUT_CHARS"; - private String IME_KEYCODE = "ADB_INPUT_CODE"; - private String IME_EDITORCODE = "ADB_EDITOR_CODE"; - private BroadcastReceiver mReceiver = null; - private InputMethodManager manager; - - @Override - public void onCreate() { - super.onCreate(); - this.manager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - } - - @Override - public View onCreateInputView() { - View mInputView = getLayoutInflater().inflate(R.layout.input_view, null); - - if (mReceiver == null) { - IntentFilter filter = new IntentFilter(IME_MESSAGE); - filter.addAction(IME_SEARCH_MESSAGE); - filter.addAction(IME_CHARS); - filter.addAction(IME_KEYCODE); - filter.addAction(IME_EDITORCODE); - mReceiver = new AdbReceiver(); - registerReceiver(mReceiver, filter); - } - - // 当出现特殊情况,没有切换回系统输入法,需要用户手动点击切换 - mInputView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - manager.showInputMethodPicker(); - } - }); - - return mInputView; - } - - public void onDestroy() { - if (mReceiver != null) { - unregisterReceiver(mReceiver); - } - - super.onDestroy(); - } - - class AdbReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - boolean sendFlag = false; - - if (intent.getAction().equals(IME_MESSAGE)) { - String msg = intent.getStringExtra("msg"); - if (msg != null) { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - ic.commitText(msg, 1); - sendFlag = true; - } - } - } - - // 输入并搜索 - if (intent.getAction().equals(IME_SEARCH_MESSAGE)) { - String msg = intent.getStringExtra("msg"); - if (msg != null) { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - sendFlag = true; - - ic.commitText(msg, 1); - - // 需要额外点击发送 - EditorInfo editorInfo = getCurrentInputEditorInfo(); - if (editorInfo != null) { - int options = editorInfo.imeOptions; - final int actionId = options & EditorInfo.IME_MASK_ACTION; - - switch (actionId) { - case EditorInfo.IME_ACTION_SEARCH: - sendDefaultEditorAction(true); - break; - case EditorInfo.IME_ACTION_GO: - sendDefaultEditorAction(true); - break; - case EditorInfo.IME_ACTION_SEND: - sendDefaultEditorAction(true); - break; - default: - ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER)); - } - } - } - } - } - - if (intent.getAction().equals(IME_CHARS)) { - int[] chars = intent.getIntArrayExtra("chars"); - if (chars != null) { - String msg = new String(chars, 0, chars.length); - InputConnection ic = getCurrentInputConnection(); - if (ic != null){ - ic.commitText(msg, 1); - sendFlag = true; - } - } - } - - if (intent.getAction().equals(IME_KEYCODE)) { - int code = intent.getIntExtra("code", -1); - if (code != -1) { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - ic.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, code)); - sendFlag = true; - } - } - } - - if (intent.getAction().equals(IME_EDITORCODE)) { - int code = intent.getIntExtra("code", -1); - if (code != -1) { - InputConnection ic = getCurrentInputConnection(); - if (ic != null) { - ic.performEditorAction(code); - sendFlag = true; - } - } - } - - // 进行了输入,发广播通知切换回原始输入法 - if (sendFlag) { - String defaultIme = intent.getStringExtra("default"); - if (defaultIme == null) { - defaultIme = MyApplication.getCurSysInputMethod(); - } - if (!StringUtil.isEmpty(defaultIme)) { - final String finalDefaultIme = defaultIme; - // 两秒后切回原始输入法 - BackgroundExecutor.execute(new Runnable() { - @Override - public void run() { - CmdTools.execAdbCmd("settings put secure default_input_method " + finalDefaultIme, 2000); - OperationService service = LauncherApplication.getInstance().findServiceByName(OperationService.class.getName()); - - MiscUtil.sleep(1000); - // 1.5s后检查下是否需要隐藏输入法 - service.doSomeAction(new OperationMethod(PerformActionEnum.HIDE_INPUT_METHOD), null); - } - }, 500); - } - } - } - } -} diff --git a/src/app/src/main/java/com/alipay/hulu/tools/HighLightService.java b/src/app/src/main/java/com/alipay/hulu/tools/HighLightService.java index a3854c9..1141fb0 100644 --- a/src/app/src/main/java/com/alipay/hulu/tools/HighLightService.java +++ b/src/app/src/main/java/com/alipay/hulu/tools/HighLightService.java @@ -47,14 +47,14 @@ public class HighLightService implements ExportService, View.OnTouchListener { private static final String TAG = "HighLightService"; - private static int WINDOW_LEVEL = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; + private static int WINDOW_LEVEL = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY: WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; Context cx = null; private WeakReference windowViewRef = null; private WindowManager wm; public Handler mHandler; - public View unvisiableView; + public View invisibleView; @Override public void onCreate(Context context) { @@ -63,18 +63,19 @@ public void onCreate(Context context) { mHandler = new Handler(); - unvisiableView = new View(cx); + invisibleView = new View(cx); int targetColor; if (Build.VERSION.SDK_INT >= 23) { targetColor = context.getColor(R.color.colorAccent); } else { targetColor = context.getResources().getColor(R.color.colorAccent); } - unvisiableView.setBackgroundColor(targetColor); + invisibleView.setBackgroundColor(targetColor); WindowManager.LayoutParams params = new WindowManager.LayoutParams(); //创建非模态、不可碰触 params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL - |WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; //放在左上角 params.gravity = Gravity.START | Gravity.TOP; params.height = 1; @@ -83,12 +84,12 @@ public void onCreate(Context context) { params.type = WINDOW_LEVEL; try { - wm.addView(unvisiableView, params); + wm.addView(invisibleView, params); } catch (WindowManager.BadTokenException e) { LogUtil.e(TAG, e, "无法使用Window type = %d, 降级", WINDOW_LEVEL); WINDOW_LEVEL = TYPE_TOAST; params.type = WINDOW_LEVEL; - wm.addView(unvisiableView, params); + wm.addView(invisibleView, params); } } @@ -99,6 +100,8 @@ public void onDestroy(Context context) { } this.cx = null; this.mHandler = null; + + wm.removeViewImmediate(invisibleView); } /** @@ -110,7 +113,12 @@ public void highLight(final Rect displayRect, final Point point) { mHandler.post(new Runnable() { @Override public void run() { - makeWindow(displayRect, point); + try { + makeWindow(displayRect, point); + } catch (Throwable t) { + // 闪退避免 + LogUtil.e(TAG, "抛出异常: " + t.getMessage(), t); + } } }); } @@ -130,7 +138,9 @@ private synchronized void makeWindow(Rect displayRect, Point clickPos) { // 拿一下高亮框引用 View windowView; + boolean update = true; if (windowViewRef == null || (windowView = windowViewRef.get())== null) { + update = false; windowView = LayoutInflater.from(cx).inflate(R.layout.highlight_win, null); windowView.setOnTouchListener(this); windowViewRef = new WeakReference<>(windowView); @@ -157,12 +167,14 @@ private synchronized void makeWindow(Rect displayRect, Point clickPos) { // 记录下状态栏高度 int[] xAndY = new int[] {0, 0}; - unvisiableView.getLocationOnScreen(xAndY); + invisibleView.getLocationOnScreen(xAndY); // 设置下windowParam WindowManager.LayoutParams wmParams = ((MyApplication) cx.getApplicationContext()).getMywmParams(); - wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; - wmParams.flags |= 8; + wmParams.type = WINDOW_LEVEL; + wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; wmParams.gravity = Gravity.LEFT | Gravity.TOP; // 调整悬浮窗口至左上角 // 以屏幕左上角为原点,设置x、y初始值 wmParams.x = displayRect.left - xAndY[0]; @@ -173,9 +185,14 @@ private synchronized void makeWindow(Rect displayRect, Point clickPos) { wmParams.format = PixelFormat.RGBA_8888; try { - wm.addView(windowView, wmParams); + if (update) { + wm.updateViewLayout(windowView, wmParams); + } else { + wm.addView(windowView, wmParams); + } } catch (WindowManager.BadTokenException e) { LogUtil.e(TAG, "系统不允许显示悬浮窗", e); + wm.removeView(windowView); } catch (IllegalStateException e) { LogUtil.e(TAG, "悬浮窗已加载", e); wm.removeView(windowView); diff --git a/src/app/src/main/java/com/alipay/hulu/tools/PerformStressImpl.java b/src/app/src/main/java/com/alipay/hulu/tools/PerformStressImpl.java index 60387e5..9030cc5 100644 --- a/src/app/src/main/java/com/alipay/hulu/tools/PerformStressImpl.java +++ b/src/app/src/main/java/com/alipay/hulu/tools/PerformStressImpl.java @@ -15,47 +15,89 @@ */ package com.alipay.hulu.tools; +import android.content.Context; + +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.injector.param.Subscriber; +import com.alipay.hulu.common.injector.provider.Param; +import com.alipay.hulu.common.service.base.ExportService; +import com.alipay.hulu.common.service.base.LocalService; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.shared.display.items.MemoryTools; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; -public class PerformStressImpl implements IPerformStress { +@LocalService +public class PerformStressImpl implements ExportService { + public static final String PERFORMANCE_STRESS_CPU_COUNT = "performanceStressCpuCount"; + public static final String PERFORMANCE_STRESS_CPU_PERCENT = "performanceStressCpuPercent"; + public static final String PERFORMANCE_STRESS_MEMORY = "performanceStressMemory"; private static final String TAG = "PerformStressImpl"; - private static PerformStressImpl instance; - - public static PerformStressImpl getInstanceImpl() { - if (instance == null) { - synchronized (PerformStressImpl.class) { - if (instance == null) { - instance = new PerformStressImpl(); - } - } - } - return instance; - } ExecutorService cachedThreadPool; private AtomicInteger currentCount = new AtomicInteger(); private volatile int targetCount = 0; private int stress = 0; + private int memory = 0; - PerformStressImpl() { - cachedThreadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + @Subscriber(@Param(PERFORMANCE_STRESS_CPU_COUNT)) + public void setTargetCount(int targetCount) { + if (targetCount == this.targetCount) { + return; + } + this.targetCount = targetCount; + performCpuStressByCount(); } - public void addOrReduceToTargetThread(int count) { - + @Subscriber(@Param(PERFORMANCE_STRESS_CPU_PERCENT)) + public void setStress(int stress) { + if (stress == this.stress) { + return; + } + this.stress = stress; + performCpuStressByCount(); } - public synchronized void performCpuStressByCount(final int stress, int count) { - this.stress = stress; - this.targetCount = count; + @Subscriber(@Param(PERFORMANCE_STRESS_MEMORY)) + public void setMemory(int memory) { + if (memory == this.memory) { + return; + } + this.memory = memory; + performMemoryStress(); + } + + /** + * 内存不足时调整一下内存数据 + */ + @Subscriber(@Param(value = LauncherApplication.ON_TRIM_MEMORY, sticky = false)) + public void onTrimMemory() { + LogUtil.w(TAG, "Urgent!!!!, lower memory"); + if (memory > 0) { + int newMemory = (int) (memory * 0.8); + InjectorService.g().pushMessage(PERFORMANCE_STRESS_MEMORY, newMemory); + } + } - if (count > currentCount.get()) { - for (int i = currentCount.get() + 1; i <= count; i++) { + @Override + public void onCreate(Context context) { + cachedThreadPool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + InjectorService.g().register(this); + } + + @Override + public void onDestroy(Context context) { + cachedThreadPool.shutdownNow(); + InjectorService.g().unregister(this); + } + + public void performCpuStressByCount() { + if (targetCount > currentCount.get()) { + for (int i = 0; i < targetCount - currentCount.get(); i++) { LogUtil.d(TAG, "新建一个线程"); final int finalI = i; cachedThreadPool.execute(new Runnable() { @@ -65,7 +107,7 @@ public void run() { } }); } - currentCount.set(count); + currentCount.set(targetCount); } } @@ -100,10 +142,17 @@ void performCpuStress(int idx) { currentCount.decrementAndGet(); } - @Override - public void PerformEntry(int param) { - // TODO Auto-generated method stub + /** + * 开始性能加压 + */ + void performMemoryStress() { + try { + this.memory = MemoryTools.dummyMem(memory); + InjectorService.g().pushMessage(PERFORMANCE_STRESS_MEMORY, memory); + } catch (OutOfMemoryError e) { + LauncherApplication.getInstance().showToast("内存不足:" + e.getMessage()); + LogUtil.e(TAG, "Alloc memory throw oom: " + e.getMessage(), e); + } } - } diff --git a/src/app/src/main/java/com/alipay/hulu/ui/AnyCodeReaderView.java b/src/app/src/main/java/com/alipay/hulu/ui/AnyCodeReaderView.java new file mode 100644 index 0000000..8b06a5d --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/AnyCodeReaderView.java @@ -0,0 +1,524 @@ +/* + * Copyright 2014 David Lázaro Esparcia. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.ui; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.hardware.Camera; +import android.os.AsyncTask; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.WindowManager; + +import com.alipay.hulu.ui.scan.Orientation; +import com.alipay.hulu.ui.scan.QRToViewPointTransformer; +import com.alipay.hulu.ui.scan.camera.CameraManager; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.MultiFormatReader; +import com.google.zxing.NotFoundException; +import com.google.zxing.PlanarYUVLuminanceSource; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.HybridBinarizer; + +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static android.hardware.Camera.getCameraInfo; + +/** + * AnyCodeReaderView Class which uses ZXING lib and let you easily integrate a QR decoder view. + * Take some classes and made some modifications in the original ZXING - Barcode Scanner project. + * + * @author David Lázaro + */ +public class AnyCodeReaderView extends SurfaceView + implements SurfaceHolder.Callback, Camera.PreviewCallback { + + public interface OnCodeReadListener { + + void onCodeRead(BarcodeFormat format, String text, PointF[] points); + } + + private OnCodeReadListener mOnCodeReadListener; + + private static final String TAG = AnyCodeReaderView.class.getName(); + + private MultiFormatReader mCodeReader; + private int mPreviewWidth; + private int mPreviewHeight; + private CameraManager mCameraManager; + private boolean mQrDecodingEnabled = true; + private DecodeFrameTask decodeFrameTask; + private Map decodeHints; + + public AnyCodeReaderView(Context context) { + this(context, null); + } + + public AnyCodeReaderView(Context context, AttributeSet attrs) { + super(context, attrs); + + if (isInEditMode()) { + return; + } + + if (checkCameraHardware()) { + mCameraManager = new CameraManager(getContext()); + mCameraManager.setPreviewCallback(this); + getHolder().addCallback(this); + setBackCamera(); + } else { + throw new RuntimeException("Error: Camera not found"); + } + } + + /** + * Set the callback to return decoding result + * + * @param onCodeReadListener the listener + */ + public void setOnCodeReadListener(OnCodeReadListener onCodeReadListener) { + mOnCodeReadListener = onCodeReadListener; + } + + /** + * Set QR decoding enabled/disabled. + * default value is true + * + * @param qrDecodingEnabled decoding enabled/disabled. + */ + public void setQRDecodingEnabled(boolean qrDecodingEnabled) { + this.mQrDecodingEnabled = qrDecodingEnabled; + } + + /** + * Set QR hints required for decoding + * + * @param decodeHints hints for decoding qrcode + */ + public void setDecodeHints(Map decodeHints) { + this.decodeHints = decodeHints; + } + + /** + * Starts camera preview and decoding + */ + public void startCamera() { + mCameraManager.startPreview(); + } + + /** + * Stop camera preview and decoding + */ + public void stopCamera() { + mCameraManager.stopPreview(); + } + + /** + * Set Camera autofocus interval value + * default value is 5000 ms. + * + * @param autofocusIntervalInMs autofocus interval value + */ + public void setAutofocusInterval(long autofocusIntervalInMs) { + if (mCameraManager != null) { + mCameraManager.setAutofocusInterval(autofocusIntervalInMs); + } + } + + /** + * Trigger an auto focus + */ + public void forceAutoFocus() { + if (mCameraManager != null) { + mCameraManager.forceAutoFocus(); + } + } + + /** + * Set Torch enabled/disabled. + * default value is false + * + * @param enabled torch enabled/disabled. + */ + public void setTorchEnabled(boolean enabled) { + if (mCameraManager != null) { + mCameraManager.setTorchEnabled(enabled); + } + } + + /** + * Allows user to specify the camera ID, rather than determine + * it automatically based on available cameras and their orientation. + * + * @param cameraId camera ID of the camera to use. A negative value means "no preference". + */ + public void setPreviewCameraId(int cameraId) { + mCameraManager.setPreviewCameraId(cameraId); + } + + /** + * Camera preview from device back camera + */ + public void setBackCamera() { + setPreviewCameraId(Camera.CameraInfo.CAMERA_FACING_BACK); + } + + /** + * Camera preview from device front camera + */ + public void setFrontCamera() { + setPreviewCameraId(Camera.CameraInfo.CAMERA_FACING_FRONT); + } + + private float oldDist = 1f; + + @Override + public boolean onTouchEvent(MotionEvent event) { + Camera camera = mCameraManager.getOpenCamera().getCamera(); + if (event.getPointerCount() == 1) { + handleFocusMetering(event, camera); + } else { + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_POINTER_DOWN: + oldDist = getFingerSpacing(event); + break; + case MotionEvent.ACTION_MOVE: + float newDist = getFingerSpacing(event); + if (newDist > oldDist) { + Log.e("Camera","进入放大手势"); + handleZoom(true, camera); + } else if (newDist < oldDist) { + Log.e("Camera","进入缩小手势"); + handleZoom(false, camera); + } + oldDist = newDist; + break; + } + } + return true; + } + + private void handleZoom(boolean isZoomIn, Camera camera) { + Log.e("Camera","进入缩小放大方法"); + Camera.Parameters params = camera.getParameters(); + if (params.isZoomSupported()) { + int maxZoom = params.getMaxZoom(); + int zoom = params.getZoom(); + if (isZoomIn && zoom < maxZoom) { + Log.e("Camera","进入放大方法zoom="+zoom); + zoom++; + } else if (zoom > 0) { + Log.e("Camera","进入缩小方法zoom="+zoom); + zoom--; + } + params.setZoom(zoom); + camera.setParameters(params); + } else { + Log.i(TAG, "zoom not supported"); + } + } + + private static void handleFocusMetering(MotionEvent event, Camera camera) { + Log.e("Camera","进入handleFocusMetering"); + Camera.Parameters params = camera.getParameters(); + + Camera.Size previewSize = params.getPreviewSize(); + Rect focusRect = calculateTapArea(event.getX(), event.getY(), 1f, previewSize); + Rect meteringRect = calculateTapArea(event.getX(), event.getY(), 1.5f, previewSize); + + camera.cancelAutoFocus(); + + if (params.getMaxNumFocusAreas() > 0) { + List focusAreas = new ArrayList<>(); + focusAreas.add(new Camera.Area(focusRect, 800)); + params.setFocusAreas(focusAreas); + } else { + Log.i(TAG, "focus areas not supported"); + } + if (params.getMaxNumMeteringAreas() > 0) { + List meteringAreas = new ArrayList<>(); + meteringAreas.add(new Camera.Area(meteringRect, 800)); + params.setMeteringAreas(meteringAreas); + } else { + Log.i(TAG, "metering areas not supported"); + } + final String currentFocusMode = params.getFocusMode(); + params.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO); + camera.setParameters(params); + + camera.autoFocus(new Camera.AutoFocusCallback() { + @Override + public void onAutoFocus(boolean success, Camera camera) { + Camera.Parameters params = camera.getParameters(); + params.setFocusMode(currentFocusMode); + camera.setParameters(params); + } + }); + } + + private static float getFingerSpacing(MotionEvent event) { + float x = event.getX(0) - event.getX(1); + float y = event.getY(0) - event.getY(1); + Log.e("Camera","getFingerSpacing ,计算距离 = " + (float) Math.sqrt(x * x + y * y)); + return (float) Math.sqrt(x * x + y * y); + } + + private static Rect calculateTapArea(float x, float y, float coefficient, Camera.Size previewSize) { + float focusAreaSize = 300; + int areaSize = Float.valueOf(focusAreaSize * coefficient).intValue(); + int centerX = (int) (x / previewSize.width - 1000); + int centerY = (int) (y / previewSize.height - 1000); + + int left = clamp(centerX - areaSize / 2, -1000, 1000); + int top = clamp(centerY - areaSize / 2, -1000, 1000); + + RectF rectF = new RectF(left, top, left + areaSize, top + areaSize); + + return new Rect(Math.round(rectF.left), Math.round(rectF.top), Math.round(rectF.right), Math.round(rectF.bottom)); + } + + private static int clamp(int x, int min, int max) { + if (x > max) { + return max; + } + if (x < min) { + return min; + } + return x; + } + + @Override public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (decodeFrameTask != null) { + decodeFrameTask.cancel(true); + decodeFrameTask = null; + } + } + + /**************************************************** + * SurfaceHolder.Callback,Camera.PreviewCallback + ****************************************************/ + + @Override public void surfaceCreated(SurfaceHolder holder) { + Log.d(TAG, "surfaceCreated"); + + try { + // Indicate camera, our View dimensions + mCameraManager.openDriver(holder, this.getWidth(), this.getHeight()); + } catch (IOException | RuntimeException e) { + Log.w(TAG, "Can not openDriver: " + e.getMessage()); + mCameraManager.closeDriver(); + } + + try { + mCodeReader = new MultiFormatReader(); + mCameraManager.startPreview(); + } catch (Exception e) { + Log.e(TAG, "Exception: " + e.getMessage()); + mCameraManager.closeDriver(); + } + } + + @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Log.d(TAG, "surfaceChanged"); + + if (holder.getSurface() == null) { + Log.e(TAG, "Error: preview surface does not exist"); + return; + } + + if (mCameraManager.getPreviewSize() == null) { + Log.e(TAG, "Error: preview size does not exist"); + return; + } + + mPreviewWidth = mCameraManager.getPreviewSize().x; + mPreviewHeight = mCameraManager.getPreviewSize().y; + + mCameraManager.stopPreview(); + + // Fix the camera sensor rotation + mCameraManager.setPreviewCallback(this); + mCameraManager.setDisplayOrientation(getCameraDisplayOrientation()); + + mCameraManager.startPreview(); + } + + @Override public void surfaceDestroyed(SurfaceHolder holder) { + Log.d(TAG, "surfaceDestroyed"); + + mCameraManager.setPreviewCallback(null); + mCameraManager.stopPreview(); + mCameraManager.closeDriver(); + } + + // Called when camera take a frame + @Override public void onPreviewFrame(byte[] data, Camera camera) { + if (!mQrDecodingEnabled || decodeFrameTask != null + && (decodeFrameTask.getStatus() == AsyncTask.Status.RUNNING + || decodeFrameTask.getStatus() == AsyncTask.Status.PENDING)) { + return; + } + + decodeFrameTask = new DecodeFrameTask(this, decodeHints); + decodeFrameTask.execute(data); + } + + /** Check if this device has a camera */ + private boolean checkCameraHardware() { + if (getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)) { + // this device has a camera + return true; + } else if (getContext().getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)) { + // this device has a front camera + return true; + } else { + // this device has any camera + return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY); + } + } + + /** + * Fix for the camera Sensor on some devices (ex.: Nexus 5x) + */ + @SuppressWarnings("deprecation") private int getCameraDisplayOrientation() { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.GINGERBREAD) { + return 90; + } + + Camera.CameraInfo info = new Camera.CameraInfo(); + getCameraInfo(mCameraManager.getPreviewCameraId(), info); + WindowManager windowManager = + (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); + int rotation = windowManager.getDefaultDisplay().getRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + default: + break; + } + + int result; + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { // back-facing + result = (info.orientation - degrees + 360) % 360; + } + return result; + } + + private static class DecodeFrameTask extends AsyncTask { + + private final WeakReference viewRef; + private final WeakReference> hintsRef; + private final QRToViewPointTransformer qrToViewPointTransformer = + new QRToViewPointTransformer(); + + public DecodeFrameTask(AnyCodeReaderView view, Map hints) { + viewRef = new WeakReference<>(view); + hintsRef = new WeakReference<>(hints); + } + + @Override protected Result doInBackground(byte[]... params) { + final AnyCodeReaderView view = viewRef.get(); + if (view == null) { + return null; + } + + final PlanarYUVLuminanceSource source = + view.mCameraManager.buildLuminanceSource(params[0], view.mPreviewWidth, + view.mPreviewHeight); + + final HybridBinarizer hybBin = new HybridBinarizer(source); + final BinaryBitmap bitmap = new BinaryBitmap(hybBin); + + try { + return view.mCodeReader.decode(bitmap, hintsRef.get()); + } catch (NotFoundException e) { + Log.d(TAG, "No QR Code found"); + } finally { + view.mCodeReader.reset(); + } + + return null; + } + + @Override protected void onPostExecute(Result result) { + super.onPostExecute(result); + + final AnyCodeReaderView view = viewRef.get(); + + // Notify we found a QRCode + if (view != null && result != null && view.mOnCodeReadListener != null) { + // Transform resultPoints to View coordinates + final PointF[] transformedPoints = + transformToViewCoordinates(view, result.getResultPoints()); + view.mOnCodeReadListener.onCodeRead(result.getBarcodeFormat(), result.getText(), transformedPoints); + } + } + + /** + * Transform result to surfaceView coordinates + * + * This method is needed because coordinates are given in landscape camera coordinates when + * device is in portrait mode and different coordinates otherwise. + * + * @return a new PointF array with transformed points + */ + private PointF[] transformToViewCoordinates(AnyCodeReaderView view, ResultPoint[] resultPoints) { + int orientationDegrees = view.getCameraDisplayOrientation(); + Orientation orientation = + orientationDegrees == 90 || orientationDegrees == 270 ? Orientation.PORTRAIT + : Orientation.LANDSCAPE; + Point viewSize = new Point(view.getWidth(), view.getHeight()); + Point cameraPreviewSize = view.mCameraManager.getPreviewSize(); + boolean isMirrorCamera = + view.mCameraManager.getPreviewCameraId() == Camera.CameraInfo.CAMERA_FACING_FRONT; + + return qrToViewPointTransformer.transform(resultPoints, isMirrorCamera, orientation, viewSize, + cameraPreviewSize); + } + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/ui/CaseStepStatusView.java b/src/app/src/main/java/com/alipay/hulu/ui/CaseStepStatusView.java index f6407da..7aafa9f 100644 --- a/src/app/src/main/java/com/alipay/hulu/ui/CaseStepStatusView.java +++ b/src/app/src/main/java/com/alipay/hulu/ui/CaseStepStatusView.java @@ -22,7 +22,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; diff --git a/src/app/src/main/java/com/alipay/hulu/ui/GesturePadView.java b/src/app/src/main/java/com/alipay/hulu/ui/GesturePadView.java new file mode 100644 index 0000000..4066924 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/GesturePadView.java @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import com.alipay.hulu.R; +import com.alipay.hulu.common.utils.ContextUtil; +import com.alipay.hulu.common.utils.LogUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Created by qiaoruikai on 2019/12/6 4:50 PM. + */ +public class GesturePadView extends View { + private static final String TAG = "GesturePadView"; + /** + * 背景颜色 + */ + private Drawable backgroundRes; + + /** + * 清除键资源 + */ + private Drawable clearBtnRes; + + /** + * 清除键大小 + */ + private int clearBtnSize; + + private Rect clearBtnRect; + + /** + * 背景Paint + */ + private Paint backgroundPaint; + + /** + * 目标图像 + */ + private Drawable targetImg; + + private Drawable sourceImg; + + /** + * 手势线宽度 + */ + private int lineWidth; + + /** + * 手势线颜色 + */ + private int lineColor; + /** + * 关键点半径 + */ + private int pointRadius; + + /** + * 绘制padding + */ + private int padding; + + private int statusBarHeight; + + /** + * 触摸事件时间间隔 + */ + private int gestureActionFilter; + + /** + * 手势paint + */ + private Paint gesturePaint; + + private List points; + + + public GesturePadView(Context context) { + this(context, null); + } + + public GesturePadView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public GesturePadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initAttrs(context, attrs); + loadView(); + } + + @TargetApi(21) + public GesturePadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initAttrs(context, attrs); + loadView(); + } + + /** + * 读取参数 + * @param attrs + */ + private void initAttrs(Context context, AttributeSet attrs) { + TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.GesturePadView); + + backgroundRes = array.getDrawable(R.styleable.GesturePadView_gpv_backgroundRes); + if (backgroundRes == null) { + backgroundRes = new ColorDrawable(context.getResources().getColor(R.color.textColorLowGray)); + } + backgroundPaint = new Paint(); + backgroundPaint.setStyle(Paint.Style.FILL); + backgroundPaint.setColor(Color.rgb(200, 200, 200)); + + clearBtnRes = array.getDrawable(R.styleable.GesturePadView_gpv_clearBtn); + if (clearBtnRes == null) { + clearBtnRes = context.getResources().getDrawable(R.drawable.case_edit); + } + clearBtnSize = array.getDimensionPixelSize(R.styleable.GesturePadView_gpv_clearBtnSize, + ContextUtil.dip2px(context, 36)); + clearBtnRect = new Rect(); + + padding = array.getDimensionPixelSize(R.styleable.GesturePadView_gpv_padding, + ContextUtil.dip2px(context, 2)); + + sourceImg = array.getDrawable(R.styleable.GesturePadView_gpv_targetImgRes); + + // 手势Paint配置 + lineColor = array.getColor(R.styleable.GesturePadView_gpv_lineColor, + context.getResources().getColor(R.color.colorAccent)); + lineWidth = array.getDimensionPixelSize(R.styleable.GesturePadView_gpv_lineWidth, + ContextUtil.dip2px(context, 2)); + reloadGesturePaint(); + pointRadius = array.getDimensionPixelSize(R.styleable.GesturePadView_gpv_pointRadius, + ContextUtil.dip2px(context, 2)); + + gestureActionFilter = array.getInt(R.styleable.GesturePadView_gpv_gestureFilter, 25); + + points = new ArrayList<>(); + + array.recycle(); + } + + private void loadView() { + // 获取标题栏高度 + if (statusBarHeight == 0) { + try { + Class clazz = Class.forName("com.android.internal.R$dimen"); + Object object = clazz.newInstance(); + statusBarHeight = Integer.parseInt(clazz.getField("status_bar_height") + .get(object).toString()); + statusBarHeight = getResources().getDimensionPixelSize(statusBarHeight); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (statusBarHeight == 0) { + statusBarHeight = 50; + } + } + } + } + + /** + * 获取操作路径 + * @return + */ + public List getGesturePath() { + if (targetImg == null) { + return null; + } + + if (points.size() == 0) { + return Collections.emptyList(); + } + + Rect rect = targetImg.getBounds(); + List pointFS = new ArrayList<>(points.size() + 1); + for (Point p: points) { + pointFS.add(new PointF((p.x - (float) rect.left)/ rect.width(), (p.y - (float) rect.top)/ rect.height())); + } + + return pointFS; + } + + /** + * 设置触摸事件时间间隔 + * @param gestureFilter + */ + public void setGestureFilter(int gestureFilter) { + this.gestureActionFilter = gestureFilter; + } + + /** + * 获取触摸事件时间间隔 + * @return + */ + public int getGestureFilter() { + return gestureActionFilter; + } + + public void clear() { + points.clear(); + invalidate(); + } + + private void reloadGesturePaint() { + if (gesturePaint != null) { + gesturePaint.reset(); + } else { + gesturePaint = new Paint(); + } + + gesturePaint.setColor(lineColor); + gesturePaint.setStrokeWidth(lineWidth); + gesturePaint.setStyle(Paint.Style.FILL_AND_STROKE); + gesturePaint.setAntiAlias(true); + gesturePaint.setFilterBitmap(false); + } + + /** + * 加载操作图片 + * @param drawable + */ + public void setTargetImage(@NonNull Drawable drawable) { + targetImg = null; + sourceImg = drawable; + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int maxWidth = MeasureSpec.getSize(widthMeasureSpec); + int maxHeight = MeasureSpec.getSize(heightMeasureSpec); + if (maxHeight > maxWidth) { + // 设置容器所需的宽度和高度 + setMeasuredDimension(maxWidth, maxWidth); + } else { + setMeasuredDimension(maxHeight, maxHeight); + } + } + + private int oldWidth = -1; + + private void reloadWidth(int width) { + oldWidth = width; + backgroundRes.setBounds(0, 0 ,width, width); + clearBtnRect.set(width - clearBtnSize - padding, padding, width - padding, clearBtnSize + padding); + int padding = (int) (clearBtnRect.height() * 0.2F); + clearBtnRes.setBounds(clearBtnRect.left + padding, clearBtnRect.top + padding, + clearBtnRect.right - padding, clearBtnRect.bottom - padding); + } + + private void loadTargetImg(int totalWidth) { + int size = totalWidth - padding * 2; + int width = sourceImg.getIntrinsicWidth(); + int height = sourceImg.getIntrinsicHeight(); + + LogUtil.d(TAG, "Image info: w:%d, h:%d, s:%d", width, height, size); + Bitmap realBitmap; + if (sourceImg instanceof BitmapDrawable) { + realBitmap = ((BitmapDrawable) sourceImg).getBitmap(); + } else { + realBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444); + Canvas canvas = new Canvas(realBitmap); + sourceImg.setBounds(0, 0, width, height); + sourceImg.draw(canvas); + } + sourceImg = null; + + float radio = width / (float) height; + if (width > height) { + float scaledHeight = size / radio; + Bitmap scaled = Bitmap.createScaledBitmap(realBitmap, size, (int) scaledHeight, false); + targetImg = new BitmapDrawable(getResources(), scaled); + targetImg.setBounds(padding, (int) (size / 2 - scaledHeight / 2), size + padding, (int) (size / 2 + scaledHeight / 2)); + } else if (width == height) { + targetImg = new BitmapDrawable(getResources(), realBitmap); + targetImg.setBounds(padding, padding, size + padding, size + padding); + } else { + float scaledWidth = size * radio; + Bitmap scaled = Bitmap.createScaledBitmap(realBitmap, (int) scaledWidth, size, false); + targetImg = new BitmapDrawable(getResources(), scaled); + targetImg.setBounds((int) (size / 2 - scaledWidth / 2), padding, (int) (size / 2 + scaledWidth / 2), size + padding); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + int width = getWidth(); + + if (width != oldWidth) { + reloadWidth(width); + } + + if (targetImg == null && sourceImg != null) { + loadTargetImg(width); + } + + backgroundRes.draw(canvas); +// canvas.save(); + + if (targetImg != null) { + targetImg.draw(canvas); + } + + canvas.saveLayerAlpha(0, 0, width, width, 125, Canvas.ALL_SAVE_FLAG); +// canvas.save(); + canvas.drawRect(clearBtnRect, backgroundPaint); + clearBtnRes.draw(canvas); + canvas.restore(); + + drawPoints(canvas); + } + + private void drawPoints(Canvas canvas) { + if (points != null && points.size() > 0) { + int i; + for (i = 0; i < points.size() - 1; i++) { + Point p1 = points.get(i); + Point p2 = points.get(i + 1); + + canvas.drawLine(p1.x, p1.y, p2.x, p2.y, gesturePaint); + if (pointRadius > 0) { + canvas.drawCircle(p1.x, p1.y, pointRadius, gesturePaint); + } + } + + if (pointRadius > 0) { + canvas.drawCircle(points.get(i).x, points.get(i).y, pointRadius, gesturePaint); + } + } + } + + private long lastBtnTime = -1L; + + private boolean onPointTrack = false; + private long lastPointTime = -1L; + + @Override + public boolean onTouchEvent(MotionEvent event) { + int[] originScreen = new int[2]; + getLocationInWindow(originScreen); + int x = (int) event.getX(); + int y = (int) event.getY(); + + LogUtil.d(TAG, "Action x: %d, y: %d", x, y); + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (clearBtnRect.contains(x, y)) { + lastBtnTime = System.currentTimeMillis(); + onPointTrack = false; + } else if (targetImg != null && targetImg.getBounds().contains(x, y)) { + points.add(new Point(x, y)); + invalidate(); + onPointTrack = true; + lastPointTime = System.currentTimeMillis(); + lastBtnTime = -1L; + } else { + return false; + } + return true; + case MotionEvent.ACTION_MOVE: + if (lastBtnTime > -1) { + if (!clearBtnRect.contains(x, y)) { + lastBtnTime = -1; + } + } else if (onPointTrack) { + if (targetImg.getBounds().contains(x, y)) { + if (System.currentTimeMillis() - lastPointTime >= gestureActionFilter) { + + // 长按fix + int count = (int) ((System.currentTimeMillis() - lastPointTime) / gestureActionFilter); + if (count > 1) { + Point last = points.get(points.size() - 1); + for (int i = 1; i < count; i++) { + points.add(last); + } + } + + points.add(new Point(x, y)); + lastPointTime = System.currentTimeMillis(); + invalidate(); + } + } else { + onPointTrack = false; + } + } + break; + case MotionEvent.ACTION_UP: + if (lastBtnTime > -1) { + if (!clearBtnRect.contains(x, y)) { + lastBtnTime = -1; + } else { + points.clear(); + invalidate(); + } + } else if (onPointTrack) { + if (targetImg.getBounds().contains(x, y)) { + if (System.currentTimeMillis() - lastPointTime >= gestureActionFilter) { + int count = (int) ((System.currentTimeMillis() - lastPointTime) / gestureActionFilter); + Point last = points.get(points.size() - 1); + for (int i = 0; i < count; i++) { + points.add(last); + } + } + points.add(new Point(x, y)); + lastPointTime = -1L; + invalidate(); + } + onPointTrack = false; + } + break; + } + return false; + } +} \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/ui/HeadControlPanel.java b/src/app/src/main/java/com/alipay/hulu/ui/HeadControlPanel.java index 677ff2b..b776914 100644 --- a/src/app/src/main/java/com/alipay/hulu/ui/HeadControlPanel.java +++ b/src/app/src/main/java/com/alipay/hulu/ui/HeadControlPanel.java @@ -25,15 +25,16 @@ import android.widget.TextView; import com.alipay.hulu.R; -import com.alipay.hulu.common.utils.ContextUtil; public class HeadControlPanel extends RelativeLayout { + public static final int POSITION_CENTER = 0; + public static final int POSITION_LEFT = 1; + public static final int POSITION_RIGHT = 2; private TextView mMidleTitle; private ImageView infoIcon; private ImageView backIcon; private LinearLayout headMenuLayout; - private static final float middle_title_size = 20f; public HeadControlPanel(Context context, AttributeSet attrs) { super(context, attrs); @@ -72,8 +73,18 @@ public void addMenuFromLeft(View v) { real = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } - // 保证右侧16DP间距 - real.setMarginEnd(ContextUtil.dip2px(getContext(), 16)); + if (v instanceof ImageView) { + // 限制为40dp + if (real.width > 0) { + real.width = getResources().getDimensionPixelSize(R.dimen.control_dp40); + } + if (real.height > 0) { + real.height = getResources().getDimensionPixelSize(R.dimen.control_dp40); + } + } + + // 保证右侧4DP间距 + real.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.control_dp8)); v.setLayoutParams(real); headMenuLayout.addView(v, 0); @@ -96,17 +107,55 @@ public void addMenuFromRight(View v) { real = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } - // 保证左侧16DP间距 - real.setMarginStart(ContextUtil.dip2px(getContext(), 16)); + if (v instanceof ImageView) { + // 限制为40dp + if (real.width > 0) { + real.width = getResources().getDimensionPixelSize(R.dimen.control_dp40); + } + if (real.height > 0) { + real.height = getResources().getDimensionPixelSize(R.dimen.control_dp40); + } + } + + // 保证左侧4DP间距 + real.setMarginStart(getResources().getDimensionPixelSize(R.dimen.control_dp8)); v.setLayoutParams(real); headMenuLayout.addView(v); } + /** + * 设置标题位置 + * @param position + */ + public void setTitlePosition(int position) { + if (position == POSITION_LEFT) { + RelativeLayout.LayoutParams layoutParams = (LayoutParams) mMidleTitle.getLayoutParams(); + layoutParams.removeRule(CENTER_IN_PARENT); + layoutParams.removeRule(LEFT_OF); + layoutParams.addRule(CENTER_VERTICAL); + layoutParams.addRule(RIGHT_OF, R.id.back_icon); + mMidleTitle.setLayoutParams(layoutParams); + } else if (position == POSITION_CENTER) { + RelativeLayout.LayoutParams layoutParams = (LayoutParams) mMidleTitle.getLayoutParams(); + layoutParams.addRule(CENTER_IN_PARENT); + layoutParams.removeRule(CENTER_VERTICAL); + layoutParams.removeRule(LEFT_OF); + layoutParams.removeRule(RIGHT_OF); + mMidleTitle.setLayoutParams(layoutParams); + } else if (position == POSITION_RIGHT) { + RelativeLayout.LayoutParams layoutParams = (LayoutParams) mMidleTitle.getLayoutParams(); + layoutParams.removeRule(CENTER_IN_PARENT); + layoutParams.removeRule(RIGHT_OF); + layoutParams.addRule(CENTER_VERTICAL); + layoutParams.addRule(LEFT_OF, R.id.head_info_menu_layout); + mMidleTitle.setLayoutParams(layoutParams); + } + } + public void setMiddleTitle(String s){ mMidleTitle.setText(s); - mMidleTitle.setTextSize(middle_title_size); } public void setBackIconClickListener(OnClickListener listener) { @@ -115,6 +164,12 @@ public void setBackIconClickListener(OnClickListener listener) { backIcon.setOnClickListener(listener); } + public void setLeftIconClickListener(int drawableId, OnClickListener listener) { + backIcon.setImageResource(drawableId); + backIcon.setVisibility(VISIBLE); + backIcon.setOnClickListener(listener); + } + public void setInfoIconClickListener(int drawableId,OnClickListener listener) { infoIcon.setImageResource(drawableId); infoIcon.setVisibility(VISIBLE); diff --git a/src/app/src/main/java/com/alipay/hulu/ui/RecycleViewDivider.java b/src/app/src/main/java/com/alipay/hulu/ui/RecycleViewDivider.java index 768ecb6..bc2fbdc 100644 --- a/src/app/src/main/java/com/alipay/hulu/ui/RecycleViewDivider.java +++ b/src/app/src/main/java/com/alipay/hulu/ui/RecycleViewDivider.java @@ -21,9 +21,9 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.support.v4.content.ContextCompat; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import android.view.View; /** diff --git a/src/app/src/main/java/com/alipay/hulu/ui/ReverseImageView.java b/src/app/src/main/java/com/alipay/hulu/ui/ReverseImageView.java index a7dd6b8..4d6b3fe 100644 --- a/src/app/src/main/java/com/alipay/hulu/ui/ReverseImageView.java +++ b/src/app/src/main/java/com/alipay/hulu/ui/ReverseImageView.java @@ -25,8 +25,8 @@ import android.graphics.PorterDuffXfermode; import android.graphics.drawable.Drawable; import android.os.Build; -import android.support.annotation.Nullable; -import android.support.v7.widget.AppCompatImageView; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatImageView; import android.util.AttributeSet; import com.alipay.hulu.R; @@ -123,7 +123,7 @@ private void restoreImage() { */ private static Bitmap getBitmap(Context context,int vectorDrawableId) { Bitmap bitmap; - if (Build.VERSION.SDK_INT>Build.VERSION_CODES.LOLLIPOP){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ Drawable vectorDrawable = context.getDrawable(vectorDrawableId); bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); diff --git a/src/app/src/main/java/com/alipay/hulu/ui/TwoLevelSelectLayout.java b/src/app/src/main/java/com/alipay/hulu/ui/TwoLevelSelectLayout.java index 69693a8..ef88a4d 100644 --- a/src/app/src/main/java/com/alipay/hulu/ui/TwoLevelSelectLayout.java +++ b/src/app/src/main/java/com/alipay/hulu/ui/TwoLevelSelectLayout.java @@ -18,7 +18,7 @@ import android.annotation.TargetApi; import android.content.Context; import android.os.Build; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -46,10 +46,10 @@ public class TwoLevelSelectLayout extends LinearLayout { private static final String TAG = "TwoLevelSelectLayout"; - private List keys = new ArrayList<>(); + private List keys = new ArrayList<>(); private List icons = new ArrayList<>(); private List currentSecondLevelItems = new ArrayList<>(); - private Map> allSecondLevelItems = new HashMap<>(); + private Map> allSecondLevelItems = new HashMap<>(); private ListView firstLevel; private ListView secondLevel; @@ -106,7 +106,10 @@ private void initView(Context context, AttributeSet attrs, int style) { firstLevel = new ListView(styledContext); firstLevel.setVerticalScrollBarEnabled(false); - LayoutParams params = new LayoutParams(ContextUtil.dip2px(context, 40), ViewGroup.LayoutParams.MATCH_PARENT); + LayoutParams params = new LayoutParams( + getResources().getDimensionPixelSize(R.dimen.float_group_icon_gap) + + getResources().getDimensionPixelSize(R.dimen.float_group_icon_size), + ViewGroup.LayoutParams.MATCH_PARENT); addView(firstLevel, params); // 分割线 @@ -151,7 +154,9 @@ public View getView(int position, View convertView, ViewGroup parent) { // 设置图标 ImageView icon = (ImageView) convertView.findViewById(R.id.first_level_icon); + TextView title = (TextView) convertView.findViewById(R.id.first_level_text); icon.setImageResource(icons.get(position)); + title.setText(keys.get(position)); return convertView; } @@ -247,7 +252,7 @@ public void onItemClick(AdapterView parent, View view, int position, long id) * @param resources * @param secondLevels */ - public void updateMenus(List keys, List resources, Map> secondLevels) { + public void updateMenus(List keys, List resources, Map> secondLevels) { // 要求key与icon一一对应 if (keys == null || resources == null || keys.size() != resources.size()) { return; diff --git a/src/app/src/main/java/com/alipay/hulu/ui/scan/Orientation.java b/src/app/src/main/java/com/alipay/hulu/ui/scan/Orientation.java new file mode 100644 index 0000000..9582bd1 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/scan/Orientation.java @@ -0,0 +1,5 @@ +package com.alipay.hulu.ui.scan; + +public enum Orientation { + PORTRAIT, LANDSCAPE +} \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/ui/scan/QRToViewPointTransformer.java b/src/app/src/main/java/com/alipay/hulu/ui/scan/QRToViewPointTransformer.java new file mode 100644 index 0000000..6eba162 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/scan/QRToViewPointTransformer.java @@ -0,0 +1,51 @@ +package com.alipay.hulu.ui.scan; + +import android.graphics.Point; +import android.graphics.PointF; + +import com.google.zxing.ResultPoint; + +public class QRToViewPointTransformer { + + public PointF[] transform(ResultPoint[] qrPoints, boolean isMirrorPreview, + Orientation orientation, + Point viewSize, Point cameraPreviewSize) { + PointF[] transformedPoints = new PointF[qrPoints.length]; + int index = 0; + for (ResultPoint qrPoint : qrPoints) { + PointF transformedPoint = transform(qrPoint, isMirrorPreview, orientation, viewSize, + cameraPreviewSize); + transformedPoints[index] = transformedPoint; + index++; + } + return transformedPoints; + } + + public PointF transform(ResultPoint qrPoint, boolean isMirrorPreview, Orientation orientation, + Point viewSize, Point cameraPreviewSize) { + float previewX = cameraPreviewSize.x; + float previewY = cameraPreviewSize.y; + + PointF transformedPoint = null; + float scaleX; + float scaleY; + + if (orientation == Orientation.PORTRAIT) { + scaleX = viewSize.x / previewY; + scaleY = viewSize.y / previewX; + transformedPoint = new PointF((previewY - qrPoint.getY()) * scaleX, qrPoint.getX() * scaleY); + if (isMirrorPreview) { + transformedPoint.y = viewSize.y - transformedPoint.y; + } + } else if (orientation == Orientation.LANDSCAPE) { + scaleX = viewSize.x / previewX; + scaleY = viewSize.y / previewY; + transformedPoint = new PointF(viewSize.x - qrPoint.getX() * scaleX, + viewSize.y - qrPoint.getY() * scaleY); + if (isMirrorPreview) { + transformedPoint.x = viewSize.x - transformedPoint.x; + } + } + return transformedPoint; + } +} \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/AutoFocusManager.java b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/AutoFocusManager.java new file mode 100644 index 0000000..3abb441 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/AutoFocusManager.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * -- Class modifications + * + * Copyright 2016 David Lázaro Esparcia. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.hulu.ui.scan.camera; + +import android.hardware.Camera; +import android.os.AsyncTask; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.RejectedExecutionException; + +final class AutoFocusManager implements Camera.AutoFocusCallback { + + private static final String TAG = AutoFocusManager.class.getSimpleName(); + + protected static final long DEFAULT_AUTO_FOCUS_INTERVAL_MS = 5000L; + private static final Collection FOCUS_MODES_CALLING_AF; + private long autofocusIntervalMs = DEFAULT_AUTO_FOCUS_INTERVAL_MS; + + static { + FOCUS_MODES_CALLING_AF = new ArrayList<>(2); + FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_AUTO); + FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_MACRO); + } + + private boolean stopped; + private boolean focusing; + private final boolean useAutoFocus; + private final Camera camera; + private AsyncTask outstandingTask; + + AutoFocusManager(Camera camera) { + this.camera = camera; + String currentFocusMode = camera.getParameters().getFocusMode(); + useAutoFocus = FOCUS_MODES_CALLING_AF.contains(currentFocusMode); + Log.i(TAG, "Current focus mode '" + currentFocusMode + "'; use auto focus? " + useAutoFocus); + start(); + } + + @Override public synchronized void onAutoFocus(boolean success, Camera theCamera) { + focusing = false; + autoFocusAgainLater(); + } + + public void setAutofocusInterval(long autofocusIntervalMs) { + if (autofocusIntervalMs <= 0) { + throw new IllegalArgumentException("AutoFocusInterval must be greater than 0."); + } + this.autofocusIntervalMs = autofocusIntervalMs; + } + + private synchronized void autoFocusAgainLater() { + if (!stopped && outstandingTask == null) { + AutoFocusTask newTask = new AutoFocusTask(); + try { + newTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + outstandingTask = newTask; + } catch (RejectedExecutionException ree) { + Log.w(TAG, "Could not request auto focus", ree); + } + } + } + + synchronized void start() { + if (useAutoFocus) { + outstandingTask = null; + if (!stopped && !focusing) { + try { + camera.autoFocus(this); + focusing = true; + } catch (RuntimeException re) { + // Have heard RuntimeException reported in Android 4.0.x+; continue? + Log.w(TAG, "Unexpected exception while focusing", re); + // Try again later to keep cycle going + autoFocusAgainLater(); + } + } + } + } + + private synchronized void cancelOutstandingTask() { + if (outstandingTask != null) { + if (outstandingTask.getStatus() != AsyncTask.Status.FINISHED) { + outstandingTask.cancel(true); + } + outstandingTask = null; + } + } + + synchronized void stop() { + stopped = true; + if (useAutoFocus) { + cancelOutstandingTask(); + // Doesn't hurt to call this even if not focusing + try { + camera.cancelAutoFocus(); + } catch (RuntimeException re) { + // Have heard RuntimeException reported in Android 4.0.x+; continue? + Log.w(TAG, "Unexpected exception while cancelling focusing", re); + } + } + } + + private final class AutoFocusTask extends AsyncTask { + @Override protected Object doInBackground(Object... voids) { + try { + Thread.sleep(autofocusIntervalMs); + } catch (InterruptedException e) { + // continue + } + start(); + return null; + } + } +} \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/CameraConfigurationManager.java b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/CameraConfigurationManager.java new file mode 100644 index 0000000..e218c9e --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/CameraConfigurationManager.java @@ -0,0 +1,358 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.hulu.ui.scan.camera; + +import android.content.Context; +import android.graphics.Point; +import android.hardware.Camera; +import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; + +import com.alipay.hulu.ui.scan.camera.open.CameraFacing; +import com.alipay.hulu.ui.scan.camera.open.OpenCamera; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * A class which deals with reading, parsing, and setting the camera parameters which are used to + * configure the camera hardware. + */ +final class CameraConfigurationManager { + + private static final String TAG = "CameraConfiguration"; + + // This is bigger than the size of a small screen, which is still supported. The routine + // below will still select the default (presumably 320x240) size for these. This prevents + // accidental selection of very low resolution on some devices. + private static final int MIN_PREVIEW_PIXELS = 470 * 320; // normal screen + private static final int MAX_PREVIEW_PIXELS = 1280 * 720; + private static final float MAX_EXPOSURE_COMPENSATION = 1.5f; + private static final float MIN_EXPOSURE_COMPENSATION = 0.0f; + private final Context context; + + private Point resolution; + private Point cameraResolution; + private Point bestPreviewSize; + private Point previewSizeOnScreen; + private int cwRotationFromDisplayToCamera; + private int cwNeededRotation; + + CameraConfigurationManager(Context context) { + this.context = context; + } + + void initFromCameraParameters(OpenCamera camera, int width, int height) { + Camera.Parameters parameters = camera.getCamera().getParameters(); + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = manager.getDefaultDisplay(); + + int displayRotation = display.getRotation(); + int cwRotationFromNaturalToDisplay; + switch (displayRotation) { + case Surface.ROTATION_0: + cwRotationFromNaturalToDisplay = 0; + break; + case Surface.ROTATION_90: + cwRotationFromNaturalToDisplay = 90; + break; + case Surface.ROTATION_180: + cwRotationFromNaturalToDisplay = 180; + break; + case Surface.ROTATION_270: + cwRotationFromNaturalToDisplay = 270; + break; + default: + // Have seen this return incorrect values like -90 + if (displayRotation % 90 == 0) { + cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360; + } else { + throw new IllegalArgumentException("Bad rotation: " + displayRotation); + } + } + Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay); + + int cwRotationFromNaturalToCamera = camera.getOrientation(); + Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera); + + // Still not 100% sure about this. But acts like we need to flip this: + if (camera.getFacing() == CameraFacing.FRONT) { + cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360; + Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera); + } + + cwRotationFromDisplayToCamera = + (360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360; + Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera); + if (camera.getFacing() == CameraFacing.FRONT) { + Log.i(TAG, "Compensating rotation for front camera"); + cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360; + } else { + cwNeededRotation = cwRotationFromDisplayToCamera; + } + Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation); + + resolution = new Point(width, height); + Log.i(TAG, "Screen resolution in current orientation: " + resolution); + cameraResolution = findBestPreviewSizeValue(parameters, resolution); + Log.i(TAG, "Camera resolution: " + cameraResolution); + bestPreviewSize = findBestPreviewSizeValue(parameters, resolution); + Log.i(TAG, "Best available preview size: " + bestPreviewSize); + + boolean isScreenPortrait = resolution.x < resolution.y; + boolean isPreviewSizePortrait = bestPreviewSize.x < bestPreviewSize.y; + + if (isScreenPortrait == isPreviewSizePortrait) { + previewSizeOnScreen = bestPreviewSize; + } else { + previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x); + } + Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen); + } + + void setDesiredCameraParameters(OpenCamera camera, boolean safeMode) { + + Camera theCamera = camera.getCamera(); + Camera.Parameters parameters = theCamera.getParameters(); + + if (parameters == null) { + Log.w(TAG, + "Device error: no camera parameters are available. Proceeding without configuration."); + return; + } + + Log.i(TAG, "Initial camera parameters: " + parameters.flatten()); + + if (safeMode) { + Log.w(TAG, "In camera config safe mode -- most settings will not be honored"); + } + + // Maybe selected auto-focus but not available, so fall through here: + String focusMode = null; + if (!safeMode) { + List supportedFocusModes = parameters.getSupportedFocusModes(); + focusMode = + findSettableValue("focus mode", supportedFocusModes, Camera.Parameters.FOCUS_MODE_AUTO); + } + if (focusMode != null) { + parameters.setFocusMode(focusMode); + } + + parameters.setPreviewSize(bestPreviewSize.x, bestPreviewSize.y); + + theCamera.setParameters(parameters); + + theCamera.setDisplayOrientation(cwRotationFromDisplayToCamera); + + Camera.Parameters afterParameters = theCamera.getParameters(); + Camera.Size afterSize = afterParameters.getPreviewSize(); + if (afterSize != null && (bestPreviewSize.x != afterSize.width + || bestPreviewSize.y != afterSize.height)) { + Log.w(TAG, + "Camera said it supported preview size " + + bestPreviewSize.x + + 'x' + + bestPreviewSize.y + + ", but after setting it, preview size is " + + afterSize.width + + 'x' + + afterSize.height); + bestPreviewSize.x = afterSize.width; + bestPreviewSize.y = afterSize.height; + } + } + + Point getCameraResolution() { + return cameraResolution; + } + + Point getScreenResolution() { + return resolution; + } + + // All references to Torch are removed from here, methods, variables... + + public Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) { + + List rawSupportedSizes = parameters.getSupportedPreviewSizes(); + if (rawSupportedSizes == null) { + Log.w(TAG, "Device returned no supported preview sizes; using default"); + Camera.Size defaultSize = parameters.getPreviewSize(); + return new Point(defaultSize.width, defaultSize.height); + } + + // Sort by size, descending + List supportedPreviewSizes = new ArrayList(rawSupportedSizes); + Collections.sort(supportedPreviewSizes, new Comparator() { + @Override public int compare(Camera.Size a, Camera.Size b) { + int aPixels = a.height * a.width; + int bPixels = b.height * b.width; + if (bPixels < aPixels) { + return -1; + } + if (bPixels > aPixels) { + return 1; + } + return 0; + } + }); + + if (Log.isLoggable(TAG, Log.INFO)) { + StringBuilder previewSizesString = new StringBuilder(); + for (Camera.Size supportedPreviewSize : supportedPreviewSizes) { + previewSizesString.append(supportedPreviewSize.width) + .append('x') + .append(supportedPreviewSize.height) + .append(' '); + } + Log.i(TAG, "Supported preview sizes: " + previewSizesString); + } + + Point bestSize = null; + float screenAspectRatio = (float) screenResolution.x / (float) screenResolution.y; + + float diff = Float.POSITIVE_INFINITY; + for (Camera.Size supportedPreviewSize : supportedPreviewSizes) { + int realWidth = supportedPreviewSize.width; + int realHeight = supportedPreviewSize.height; + int pixels = realWidth * realHeight; + if (pixels < MIN_PREVIEW_PIXELS || pixels > MAX_PREVIEW_PIXELS) { + continue; + } + + // This code is modified since We're using portrait mode + boolean isCandidateLandscape = realWidth > realHeight; + int maybeFlippedWidth = isCandidateLandscape ? realHeight : realWidth; + int maybeFlippedHeight = isCandidateLandscape ? realWidth : realHeight; + + if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) { + Point exactPoint = new Point(realWidth, realHeight); + Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint); + return exactPoint; + } + float aspectRatio = (float) maybeFlippedWidth / (float) maybeFlippedHeight; + float newDiff = Math.abs(aspectRatio - screenAspectRatio); + if (newDiff < diff) { + bestSize = new Point(realWidth, realHeight); + diff = newDiff; + } + } + + if (bestSize == null) { + Camera.Size defaultSize = parameters.getPreviewSize(); + bestSize = new Point(defaultSize.width, defaultSize.height); + Log.i(TAG, "No suitable preview sizes, using default: " + bestSize); + } + + Log.i(TAG, "Found best approximate preview size: " + bestSize); + return bestSize; + } + + private static String findSettableValue(String name, Collection supportedValues, + String... desiredValues) { + Log.i(TAG, "Requesting " + name + " value from among: " + Arrays.toString(desiredValues)); + Log.i(TAG, "Supported " + name + " values: " + supportedValues); + if (supportedValues != null) { + for (String desiredValue : desiredValues) { + if (supportedValues.contains(desiredValue)) { + Log.i(TAG, "Can set " + name + " to: " + desiredValue); + return desiredValue; + } + } + } + Log.i(TAG, "No supported values match"); + return null; + } + + boolean getTorchState(Camera camera) { + if (camera != null) { + Camera.Parameters parameters = camera.getParameters(); + if (parameters != null) { + String flashMode = camera.getParameters().getFlashMode(); + return flashMode != null && (Camera.Parameters.FLASH_MODE_ON.equals(flashMode) + || Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode)); + } + } + return false; + } + + void setTorchEnabled(Camera camera, boolean enabled) { + Camera.Parameters parameters = camera.getParameters(); + setTorchEnabled(parameters, enabled, false); + camera.setParameters(parameters); + } + + void setTorchEnabled(Camera.Parameters parameters, boolean enabled, boolean safeMode) { + setTorchEnabled(parameters, enabled); + + if (!safeMode) { + setBestExposure(parameters, enabled); + } + } + + public static void setTorchEnabled(Camera.Parameters parameters, boolean enabled) { + List supportedFlashModes = parameters.getSupportedFlashModes(); + String flashMode; + if (enabled) { + flashMode = + findSettableValue("flash mode", supportedFlashModes, Camera.Parameters.FLASH_MODE_TORCH, + Camera.Parameters.FLASH_MODE_ON); + } else { + flashMode = + findSettableValue("flash mode", supportedFlashModes, Camera.Parameters.FLASH_MODE_OFF); + } + if (flashMode != null) { + if (flashMode.equals(parameters.getFlashMode())) { + Log.i(TAG, "Flash mode already set to " + flashMode); + } else { + Log.i(TAG, "Setting flash mode to " + flashMode); + parameters.setFlashMode(flashMode); + } + } + } + + public static void setBestExposure(Camera.Parameters parameters, boolean lightOn) { + + int minExposure = parameters.getMinExposureCompensation(); + int maxExposure = parameters.getMaxExposureCompensation(); + float step = parameters.getExposureCompensationStep(); + if ((minExposure != 0 || maxExposure != 0) && step > 0.0f) { + // Set low when light is on + float targetCompensation = lightOn ? MIN_EXPOSURE_COMPENSATION : MAX_EXPOSURE_COMPENSATION; + int compensationSteps = Math.round(targetCompensation / step); + float actualCompensation = step * compensationSteps; + // Clamp value: + compensationSteps = Math.max(Math.min(compensationSteps, maxExposure), minExposure); + if (parameters.getExposureCompensation() == compensationSteps) { + Log.i(TAG, "Exposure compensation already set to " + compensationSteps + " / " + + actualCompensation); + } else { + Log.i(TAG, + "Setting exposure compensation to " + compensationSteps + " / " + actualCompensation); + parameters.setExposureCompensation(compensationSteps); + } + } else { + Log.i(TAG, "Camera does not support exposure compensation"); + } + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/CameraManager.java b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/CameraManager.java new file mode 100644 index 0000000..d39be66 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/CameraManager.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.hulu.ui.scan.camera; + +import android.content.Context; +import android.graphics.Point; +import android.hardware.Camera; +import android.util.Log; +import android.view.SurfaceHolder; + +import com.alipay.hulu.ui.scan.camera.open.OpenCamera; +import com.alipay.hulu.ui.scan.camera.open.OpenCameraInterface; +import com.google.zxing.PlanarYUVLuminanceSource; +import java.io.IOException; + +/** + * This object wraps the Camera service object and expects to be the only one talking to it. The + * implementation encapsulates the steps needed to take preview-sized images, which are used for + * both preview and decoding. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class CameraManager { + + private static final String TAG = CameraManager.class.getSimpleName(); + + private final Context context; + private final CameraConfigurationManager configManager; + private OpenCamera openCamera; + private AutoFocusManager autoFocusManager; + private boolean initialized; + private boolean previewing; + private Camera.PreviewCallback previewCallback; + private int displayOrientation = 0; + + // PreviewCallback references are also removed from original ZXING authors work, + // since we're using our own interface. + // FramingRects references are also removed from original ZXING authors work, + // since We're using all view size while detecting QR-Codes. + private int requestedCameraId = OpenCameraInterface.NO_REQUESTED_CAMERA; + private long autofocusIntervalInMs = AutoFocusManager.DEFAULT_AUTO_FOCUS_INTERVAL_MS; + + public CameraManager(Context context) { + this.context = context; + this.configManager = new CameraConfigurationManager(context); + } + + public void setPreviewCallback(Camera.PreviewCallback previewCallback) { + this.previewCallback = previewCallback; + + if (isOpen()) { + openCamera.getCamera().setPreviewCallback(previewCallback); + } + } + + public void setDisplayOrientation(int degrees) { + this.displayOrientation = degrees; + + if (isOpen()) { + openCamera.getCamera().setDisplayOrientation(degrees); + } + } + + public void setAutofocusInterval(long autofocusIntervalInMs) { + this.autofocusIntervalInMs = autofocusIntervalInMs; + if (autoFocusManager != null) { + autoFocusManager.setAutofocusInterval(autofocusIntervalInMs); + } + } + + public void forceAutoFocus() { + if (autoFocusManager != null) { + autoFocusManager.start(); + } + } + + public Point getPreviewSize() { + return configManager.getCameraResolution(); + } + + /** + * Opens the camera driver and initializes the hardware parameters. + * + * @param holder The surface object which the camera will draw preview frames into. + * @param height @throws IOException Indicates the camera driver failed to open. + */ + public synchronized void openDriver(SurfaceHolder holder, int width, int height) + throws IOException { + OpenCamera theCamera = openCamera; + if (!isOpen()) { + theCamera = OpenCameraInterface.open(requestedCameraId); + if (theCamera == null || theCamera.getCamera() == null) { + throw new IOException("Camera.open() failed to return object from driver"); + } + openCamera = theCamera; + } + theCamera.getCamera().setPreviewDisplay(holder); + theCamera.getCamera().setPreviewCallback(previewCallback); + theCamera.getCamera().setDisplayOrientation(displayOrientation); + + if (!initialized) { + initialized = true; + configManager.initFromCameraParameters(theCamera, width, height); + } + + Camera cameraObject = theCamera.getCamera(); + Camera.Parameters parameters = cameraObject.getParameters(); + String parametersFlattened = + parameters == null ? null : parameters.flatten(); // Save these, temporarily + try { + configManager.setDesiredCameraParameters(theCamera, false); + } catch (RuntimeException re) { + // Driver failed + Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters"); + Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened); + // Reset: + if (parametersFlattened != null) { + parameters = cameraObject.getParameters(); + parameters.unflatten(parametersFlattened); + try { + cameraObject.setParameters(parameters); + configManager.setDesiredCameraParameters(theCamera, true); + } catch (RuntimeException re2) { + // Well, darn. Give up + Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration"); + } + } + } + cameraObject.setPreviewDisplay(holder); + } + + /** + * Allows third party apps to specify the camera ID, rather than determine + * it automatically based on available cameras and their orientation. + * + * @param cameraId camera ID of the camera to use. A negative value means "no preference". + */ + public synchronized void setPreviewCameraId(int cameraId) { + requestedCameraId = cameraId; + } + + public int getPreviewCameraId() { + return requestedCameraId; + } + + /** + * @param enabled if {@code true}, light should be turned on if currently off. And vice versa. + */ + public synchronized void setTorchEnabled(boolean enabled) { + OpenCamera theCamera = openCamera; + if (theCamera != null && enabled != configManager.getTorchState(theCamera.getCamera())) { + boolean wasAutoFocusManager = autoFocusManager != null; + if (wasAutoFocusManager) { + autoFocusManager.stop(); + autoFocusManager = null; + } + configManager.setTorchEnabled(theCamera.getCamera(), enabled); + if (wasAutoFocusManager) { + autoFocusManager = new AutoFocusManager(theCamera.getCamera()); + autoFocusManager.start(); + } + } + } + + public synchronized boolean isOpen() { + return openCamera != null && openCamera.getCamera() != null; + } + + /** + * Closes the camera driver if still in use. + */ + public synchronized void closeDriver() { + if (isOpen()) { + openCamera.getCamera().release(); + openCamera = null; + // Make sure to clear these each time we close the camera, so that any scanning rect + // requested by intent is forgotten. + // framingRect = null; + // framingRectInPreview = null; + } + } + + /** + * Asks the camera hardware to begin drawing preview frames to the screen. + */ + public synchronized void startPreview() { + OpenCamera theCamera = openCamera; + if (theCamera != null && !previewing) { + theCamera.getCamera().startPreview(); + previewing = true; + autoFocusManager = new AutoFocusManager(theCamera.getCamera()); + autoFocusManager.setAutofocusInterval(autofocusIntervalInMs); + } + } + + /** + * Tells the camera to stop drawing preview frames. + */ + public synchronized void stopPreview() { + if (autoFocusManager != null) { + autoFocusManager.stop(); + autoFocusManager = null; + } + if (openCamera != null && previewing) { + openCamera.getCamera().stopPreview(); + previewing = false; + } + } + + public OpenCamera getOpenCamera() { + return openCamera; + } + + /** + * A factory method to build the appropriate LuminanceSource object based on the format + * of the preview buffers, as described by Camera.Parameters. + * + * @param data A preview frame. + * @param width The width of the image. + * @param height The height of the image. + * @return A PlanarYUVLuminanceSource instance. + */ + public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) { + return new PlanarYUVLuminanceSource(data, width, height, 0, 0, width, height, false); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/CameraFacing.java b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/CameraFacing.java new file mode 100644 index 0000000..8887267 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/CameraFacing.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2015 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.ui.scan.camera.open; + +public enum CameraFacing { + BACK, // must be value 0! + FRONT, // must be value 1! +} \ No newline at end of file diff --git a/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/OpenCamera.java b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/OpenCamera.java new file mode 100644 index 0000000..2b864af --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/OpenCamera.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.ui.scan.camera.open; + +import android.hardware.Camera; + +public final class OpenCamera { + + private final int index; + private final Camera camera; + private final CameraFacing facing; + private final int orientation; + + public OpenCamera(int index, Camera camera, CameraFacing facing, int orientation) { + this.index = index; + this.camera = camera; + this.facing = facing; + this.orientation = orientation; + } + + public Camera getCamera() { + return camera; + } + + public CameraFacing getFacing() { + return facing; + } + + public int getOrientation() { + return orientation; + } + + @Override + public String toString() { + return "Camera #" + index + " : " + facing + ',' + orientation; + } + +} diff --git a/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/OpenCameraInterface.java b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/OpenCameraInterface.java new file mode 100644 index 0000000..82b17f1 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/ui/scan/camera/open/OpenCameraInterface.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alipay.hulu.ui.scan.camera.open; + +import android.hardware.Camera; +import android.util.Log; + +/** + * Abstraction over the {@link Camera} API that helps open them and return their metadata. + */ +public final class OpenCameraInterface { + + private static final String TAG = OpenCameraInterface.class.getName(); + + private OpenCameraInterface() { + } + + /** For {@link #open(int)}, means no preference for which camera to open. */ + public static final int NO_REQUESTED_CAMERA = -1; + + /** + * Opens the requested camera with {@link Camera#open(int)}, if one exists. + * + * @param cameraId camera ID of the camera to use. A negative value + * or {@link #NO_REQUESTED_CAMERA} means "no preference", in which case a rear-facing + * camera is returned if possible or else any camera + * @return handle to {@link OpenCamera} that was opened + */ + public static OpenCamera open(int cameraId) { + + int numCameras = Camera.getNumberOfCameras(); + if (numCameras == 0) { + Log.w(TAG, "No cameras!"); + return null; + } + + boolean explicitRequest = cameraId >= 0; + + Camera.CameraInfo selectedCameraInfo = null; + int index; + if (explicitRequest) { + index = cameraId; + selectedCameraInfo = new Camera.CameraInfo(); + Camera.getCameraInfo(index, selectedCameraInfo); + } else { + index = 0; + while (index < numCameras) { + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + Camera.getCameraInfo(index, cameraInfo); + CameraFacing reportedFacing = CameraFacing.values()[cameraInfo.facing]; + if (reportedFacing == CameraFacing.BACK) { + selectedCameraInfo = cameraInfo; + break; + } + index++; + } + } + + Camera camera; + if (index < numCameras) { + Log.i(TAG, "Opening camera #" + index); + camera = Camera.open(index); + } else { + if (explicitRequest) { + Log.w(TAG, "Requested camera does not exist: " + cameraId); + camera = null; + } else { + Log.i(TAG, "No camera facing " + CameraFacing.BACK + "; returning camera #0"); + camera = Camera.open(0); + selectedCameraInfo = new Camera.CameraInfo(); + Camera.getCameraInfo(0, selectedCameraInfo); + } + } + + if (camera == null) { + return null; + } + return new OpenCamera(index, + camera, + CameraFacing.values()[selectedCameraInfo.facing], + selectedCameraInfo.orientation); + } + +} diff --git a/src/app/src/main/java/com/alipay/hulu/upgrade/PatchRequest.java b/src/app/src/main/java/com/alipay/hulu/upgrade/PatchRequest.java index a2b01ca..5ab08f1 100644 --- a/src/app/src/main/java/com/alipay/hulu/upgrade/PatchRequest.java +++ b/src/app/src/main/java/com/alipay/hulu/upgrade/PatchRequest.java @@ -36,8 +36,10 @@ import java.io.File; import java.io.IOException; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import okhttp3.Call; @@ -64,7 +66,7 @@ public class PatchRequest { /** * 更新Patch列表 */ - public static void updatePatchList() { + public static void updatePatchList(final LoadPatchCallback callback) { String storedUrl = SPService.getString(SPService.KEY_PATCH_URL, "https://raw.githubusercontent.com/alipay/SoloPi/master/.json"); // 地址为空 if (StringUtil.isEmpty(storedUrl)) { @@ -72,8 +74,9 @@ public static void updatePatchList() { return; } + String cpuAbi = DeviceInfoUtil.getCPUABIInArm(); // 替换ABI参数 - String realUrl = StringUtil.patternReplace(storedUrl, "", DeviceInfoUtil.getCPUABI()); + String realUrl = StringUtil.patternReplace(storedUrl, "", cpuAbi); LogUtil.i(TAG, "Start request patch list on: " + realUrl); @@ -82,11 +85,18 @@ public static void updatePatchList() { @Override public void onResponse(Call call, PatchResponse item) throws IOException { doUpgradePatch(item); + if (callback != null) { + callback.onLoaded(); + } } @Override public void onFailure(Call call, IOException e) { LogUtil.e(TAG, "抛出IO异常," + e.getMessage(), e); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.constant__plugin_load_fail) + e.getMessage()); + if (callback != null) { + callback.onFailed(); + } } }); } @@ -95,7 +105,7 @@ public void onFailure(Call call, IOException e) { * 解析Patch列表 * @param response */ - private static void doUpgradePatch(PatchResponse response) { + public static void doUpgradePatch(PatchResponse response) { LogUtil.i(TAG, "接收patch列表" + response); if (response == null || !StringUtil.equals(response.getStatus(), "success")) { return; @@ -131,7 +141,9 @@ private static void doUpgradePatch(PatchResponse response) { @Override public void run() { Pair assetInfo = new Pair<>(data.getName() + ".zip", data.getUrl()); - File f = AssetsManager.getAssetFile(assetInfo, null, true); + + // Use FileDownloader fail??? + File f = AssetsManager.getAssetFileWithOkHttp(assetInfo, null, true); // 下载失败 if (f == null) { @@ -141,9 +153,9 @@ public void run() { return; } try { - PatchLoadResult result = PatchProcessUtil.dynamicLoadPatch(f); - if (result != null) { - ClassUtil.installPatch(result); + PatchLoadResult patchResult = PatchProcessUtil.dynamicLoadPatch(f); + if (patchResult != null) { + ClassUtil.installPatch(patchResult); } } catch (Throwable e) { LogUtil.e(TAG, "更新插件异常", e); @@ -165,7 +177,9 @@ public void run() { @Override public void run() { Pair assetInfo = new Pair<>(data.getName() + ".zip", data.getUrl()); - File f = AssetsManager.getAssetFile(assetInfo, null, true); + + // Use FileDownloader fail??? + File f = AssetsManager.getAssetFileWithOkHttp(assetInfo, null, true); // 下载失败 if (f == null) { @@ -174,9 +188,9 @@ public void run() { return; } try { - PatchLoadResult result = PatchProcessUtil.dynamicLoadPatch(f); - if (result != null) { - ClassUtil.installPatch(result); + PatchLoadResult patchResult = PatchProcessUtil.dynamicLoadPatch(f); + if (patchResult != null) { + ClassUtil.installPatch(patchResult); } } catch (Throwable e) { LogUtil.e(TAG, "更新插件异常", e); @@ -192,7 +206,7 @@ public void run() { if (loadingCount.get() > 0) { final BaseActivity activity = (BaseActivity) LauncherApplication.getInstance().loadActivityOnTop(); if (activity != null) { - activity.showProgressDialog("加载插件中,请耐心等待"); + activity.showProgressDialog(StringUtil.getString(R.string.patch__loading_plugin_wait)); BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -208,4 +222,17 @@ public void run() { // 更新本地插件版本 ClassUtil.updateAvailablePatches(patchMap); } + + private static final Set ACCEPT_ABI = new HashSet() { + { + add("armeabi"); + add("armeabi-v7a"); + add("arm64-v8a"); + } + }; + + public interface LoadPatchCallback { + void onLoaded(); + void onFailed(); + } } diff --git a/src/app/src/main/java/com/alipay/hulu/util/CaseAppendOperationProcessor.java b/src/app/src/main/java/com/alipay/hulu/util/CaseAppendOperationProcessor.java new file mode 100644 index 0000000..5fbec36 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/util/CaseAppendOperationProcessor.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.util; + +import android.content.Context; +import android.content.Intent; +import androidx.annotation.NonNull; + +import com.alibaba.fastjson.JSON; +import com.alipay.hulu.activity.CaseEditActivity; +import com.alipay.hulu.bean.CaseStepHolder; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.shared.io.OperationStepProcessor; +import com.alipay.hulu.shared.io.bean.GeneralOperationLogBean; +import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.io.util.OperationStepUtil; +import com.alipay.hulu.shared.node.tree.export.bean.OperationStep; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by qiaoruikai on 2019-08-06 15:31. + */ +public class CaseAppendOperationProcessor implements OperationStepProcessor { + private RecordCaseInfo originCase; + private List recordSteps; + private List subSteps; + private int insertPosition = -1; + + public CaseAppendOperationProcessor(@NonNull RecordCaseInfo originCase) { + this.originCase = originCase; + + // 加载步骤信息 + GeneralOperationLogBean operationLogBean = JSON.parseObject(originCase.getOperationLog(), GeneralOperationLogBean.class); + OperationStepUtil.afterLoad(operationLogBean); + if (operationLogBean != null) { + recordSteps = operationLogBean.getSteps(); + } + if (recordSteps == null) { + recordSteps = new ArrayList<>(); + } + subSteps = new ArrayList<>(); + } + + public void setInsertPosition(int position) { + this.insertPosition = position; + } + + @Override + public void onStartRecord(RecordCaseInfo recordCaseInfo) { + + } + + @Override + public boolean onStopRecord(final Context context) { + GeneralOperationLogBean operationLogBean = new GeneralOperationLogBean(); + // 设置额外录制位置 + if (insertPosition > -1) { + recordSteps.addAll(insertPosition, subSteps); + } else { + recordSteps.addAll(subSteps); + } + operationLogBean.setSteps(recordSteps); + + originCase.setOperationLog(JSON.toJSONString(operationLogBean)); + + final int id = CaseStepHolder.storeCase(originCase); + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + Intent intent = new Intent(context, CaseEditActivity.class); + intent.putExtra(CaseEditActivity.RECORD_CASE_EXTRA, id); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + }); + return true; + } + + @Override + public void onOperationStep(int operationIdx, OperationStep step) { + subSteps.add(step); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/util/CaseReplayUtil.java b/src/app/src/main/java/com/alipay/hulu/util/CaseReplayUtil.java new file mode 100644 index 0000000..4805b28 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/util/CaseReplayUtil.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2015-present, Ant Financial Services Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alipay.hulu.util; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; + +import com.alibaba.fastjson.JSON; +import com.alipay.hulu.R; +import com.alipay.hulu.activity.MyApplication; +import com.alipay.hulu.bean.AdvanceCaseSetting; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.service.SPService; +import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.utils.ContextUtil; +import com.alipay.hulu.common.utils.GlideUtil; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.MiscUtil; +import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.replay.BatchStepProvider; +import com.alipay.hulu.replay.MultiParamStepProvider; +import com.alipay.hulu.replay.OperationStepProvider; +import com.alipay.hulu.replay.RepeatStepProvider; +import com.alipay.hulu.service.CaseReplayManager; +import com.alipay.hulu.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.node.utils.AppUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用例回放管理器 + * Created by qiaoruikai on 2019-08-21 12:41. + */ +public class CaseReplayUtil { + private static final String TAG = CaseReplayUtil.class.getSimpleName(); + + /** + * 开始回放单条用例 + * @param recordCase + */ + public static void startReplay(@NonNull final RecordCaseInfo recordCase) { + final String advanceSettings = recordCase.getAdvanceSettings(); + final AdvanceCaseSetting setting = JSON.parseObject(advanceSettings, AdvanceCaseSetting.class); + if (setting != null && !StringUtil.isEmpty(setting.getOverrideApp())) { + recordCase.setTargetAppPackage(setting.getOverrideApp()); + } else if (SPService.getBoolean(SPService.KEY_ALLOW_REPLAY_DIFFERENT_APP, false)) { + LauncherApplication.getInstance().runOnUiThread(new Runnable() { + @Override + public void run() { + selectJumpApp(LauncherApplication.getInstance().loadActivityOnTop(), new OnAppSelectListener() { + @Override + public void onAppSelect(String appPackage, String appName) { + recordCase.setTargetAppPackage(appPackage); + recordCase.setTargetAppLabel(appName); + AdvanceCaseSetting newSettings = new AdvanceCaseSetting(setting); + newSettings.setOverrideApp(appPackage); + recordCase.setAdvanceSettings(JSON.toJSONString(newSettings)); + startReplay(recordCase); + } + + @Override + public void onNothingSelect() { + AdvanceCaseSetting newSettings = new AdvanceCaseSetting(setting); + newSettings.setOverrideApp(recordCase.getTargetAppPackage()); + recordCase.setAdvanceSettings(JSON.toJSONString(newSettings)); + startReplay(recordCase); + } + }); + } + }, 100); + return; + } + CaseReplayManager manager = LauncherApplication.getInstance().findServiceByName(CaseReplayManager.class.getName()); + MyApplication.getInstance().updateAppAndNameTemp(recordCase.getTargetAppPackage(), recordCase.getTargetAppLabel()); + + // 是否重启目标应用 + if (SPService.getBoolean(SPService.KEY_RESTART_APP_ON_PLAY, true)) { + restartTargetApp(recordCase.getTargetAppPackage()); + } else { + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + AppUtil.launchTargetApp(recordCase.getTargetAppPackage()); + } + }); + } + + if (setting != null && setting.getRunningParam() != null) { + MultiParamStepProvider stepProvider = new MultiParamStepProvider(recordCase); + manager.start(stepProvider, MyApplication.MULTI_REPLAY_LISTENER); + } else { + OperationStepProvider stepProvider = new OperationStepProvider(recordCase); + manager.start(stepProvider, MyApplication.SINGLE_REPLAY_LISTENER); + } + } + + /** + * 关闭后重启应用 + * @param packageName + */ + public static void restartTargetApp(final String packageName) { + PackageInfo pkgInfo = ContextUtil.getPackageInfoByName( + LauncherApplication.getInstance(), packageName); + if (pkgInfo == null) { + return; + } + + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + AppUtil.forceStopApp(packageName); + + LogUtil.e(TAG, "强制终止应用"); + MiscUtil.sleep(500); + AppUtil.startApp(packageName); + } + }); + } + + + /** + * 开始重复回放用例 + * @param recordCase + * @param times + * @param restart 执行前重启 + */ + public static void startReplayMultiTimes(@NonNull RecordCaseInfo recordCase, int times, boolean restart) { + RepeatStepProvider stepProvider = new RepeatStepProvider(recordCase, times, restart); + MyApplication.getInstance().updateAppAndNameTemp(recordCase.getTargetAppPackage(), recordCase.getTargetAppLabel()); + CaseReplayManager manager = LauncherApplication.getInstance().findServiceByName(CaseReplayManager.class.getName()); + manager.start(stepProvider, MyApplication.MULTI_REPLAY_LISTENER); + } + + /** + * 开始回放多条用例 + * @param recordCases 用例列表 + * @param restart 执行前重启 + */ + public static void startReplayMultiCase(@NonNull List recordCases, boolean restart) { + BatchStepProvider provider = new BatchStepProvider(new ArrayList<>(recordCases), restart); + CaseReplayManager manager = LauncherApplication.getInstance().findServiceByName(CaseReplayManager.class.getName()); + manager.start(provider, MyApplication.MULTI_REPLAY_LISTENER); + } + + /** + * 选择回放应用 + * @param context + * @param listener + */ + public static void selectJumpApp(final Context context, final OnAppSelectListener listener) { + try { + View v = LayoutInflater.from(context).inflate(R.layout.dialog_select_app, null); + final ListView list = (ListView) v.findViewById(R.id.dialog_jump_app_list); + final List listPack = MyApplication.getInstance().loadAppList(); + + list.setAdapter(new BaseAdapter() { + @Override + public int getCount() { + return listPack.size(); + } + + @Override + public Object getItem(int position) { + return listPack.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View v; + if (convertView == null) { + convertView = LayoutInflater.from(context) + .inflate(R.layout.activity_choose_layout, parent, false); + } + v = convertView; + + ApplicationInfo info = listPack.get(position); + ImageView img = (ImageView) v.findViewById(R.id.choose_icon); + GlideUtil.loadIcon(context, info.packageName, img); + TextView title = (TextView) v.findViewById(R.id.choose_title); + title.setText(info.loadLabel(context.getPackageManager()).toString()); + TextView activity = (TextView) v.findViewById(R.id.choose_activity); + activity.setText(info.packageName); + return v; + } + }); + + final AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) + .setView(v) + .setNegativeButton("取消", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + LogUtil.i(TAG, "Negative " + which); + + dialog.dismiss(); + listener.onNothingSelect(); + } + }) + .create(); + + list.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + dialog.dismiss(); + ApplicationInfo applicationInfo = listPack.get(position); + String name = applicationInfo.loadLabel(context.getPackageManager()).toString(); + listener.onAppSelect(applicationInfo.packageName, name); + } + }); + + dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 + dialog.setCancelable(false); + dialog.show(); + + } catch (Exception e) { + LogUtil.e(TAG, "Jump app dialog throw exception: " + e.getMessage(), e); + } + } + + public interface OnAppSelectListener { + void onAppSelect(String appPackage, String appName); + + void onNothingSelect(); + } +} diff --git a/src/app/src/main/java/com/alipay/hulu/util/DialogUtils.java b/src/app/src/main/java/com/alipay/hulu/util/DialogUtils.java index e0ebf0c..79cb2a7 100644 --- a/src/app/src/main/java/com/alipay/hulu/util/DialogUtils.java +++ b/src/app/src/main/java/com/alipay/hulu/util/DialogUtils.java @@ -24,15 +24,17 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.AlertDialog; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import android.text.TextUtils; import android.util.DisplayMetrics; +import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.AdapterView; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -50,11 +52,12 @@ import com.alipay.hulu.shared.node.action.PerformActionEnum; import com.alipay.hulu.ui.TwoLevelSelectLayout; import com.bumptech.glide.RequestBuilder; -import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestManager; import com.bumptech.glide.request.RequestOptions; import java.io.File; +import java.net.URL; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -197,6 +200,7 @@ public static ProgressDialog showProgressDialog(final Context context, final Str public void run() { ProgressDialog progressDialog = new ProgressDialog(context, R.style.SimpleDialogTheme); progressDialog.setMessage(str); + progressDialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); progressDialog.show(); dialogs[0] = progressDialog; @@ -314,7 +318,7 @@ public boolean isEmpty() { listView.setDividerHeight(0); final AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AppDialogTheme) - .setTitle("请选择操作") + .setTitle(R.string.function__select_function) .setView(listView) .setOnCancelListener(new DialogInterface.OnCancelListener() { @Override @@ -327,7 +331,7 @@ public void onCancel(DialogInterface dialog) { @Override public void run() { final AlertDialog dialog = builder.create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(true); //点击外面区域不会让dialog消失 dialog.setCancelable(true); @@ -450,7 +454,7 @@ public void onClick(DialogInterface dialog, int which) { dialog.setTitle(null); dialog.setCanceledOnTouchOutside(false); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.show(); } @@ -462,9 +466,9 @@ public void onClick(DialogInterface dialog, int which) { * @param secondLevels * @param callback */ - public static void showLeveledFunctionView(final Context context, final List keys, + public static void showLeveledFunctionView(final Context context, final List keys, final List icons, - final Map> secondLevels, + final Map> secondLevels, final FunctionViewCallback callback) { if (callback == null) { LogUtil.e(TAG,"回调函数为空"); @@ -484,7 +488,7 @@ public static void showLeveledFunctionView(final Context context, final List data); + } + + /** + * 为多个字段配置输入框 + * + * @param title + * @param data + */ + public static void showMultipleEditDialog(Context context, final OnDialogResultListener listener, String title, List> data) { + LayoutInflater inflater = LayoutInflater.from(ContextUtil.getContextThemeWrapper( + context, R.style.AppDialogTheme)); + + ScrollView v = (ScrollView) inflater.inflate(R.layout.dialog_setting, null); + + LinearLayout view = (LinearLayout) v.getChildAt(0); + final List editTexts = new ArrayList<>(); + + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + // 对每一个字段添加EditText + for (Pair source : data) { + View editField = inflater.inflate(R.layout.item_edit_field, null); + + EditText edit = (EditText) editField.findViewById(R.id.item_edit_field_edit); + TextView name = (TextView) editField.findViewById(R.id.item_edit_field_name); + + if (StringUtil.isEmpty(source.first)) { + name.setVisibility(View.GONE); + } else { + // 配置字段 + name.setText(source.first); + } + edit.setHint(source.first); + edit.setText(source.second); + + view.addView(editField, layoutParams); + editTexts.add(edit); + } + + // 显示Dialog + new AlertDialog.Builder(context, R.style.AppDialogTheme) + .setTitle(title) + .setView(v) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + List result = new ArrayList<>(editTexts.size() + 1); + + // 获取每个编辑框的文字 + for (EditText data : editTexts) { + result.add(data.getText().toString().trim()); + } + + if (listener != null) { + listener.onDialogPositive(result); + } + dialog.dismiss(); + } + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).setCancelable(true) + .show(); + } + /** * 显示图像Dialog * @param context @@ -590,6 +663,16 @@ public static void showImageDialog(Context context, Uri content) { dialog.show(); } + /** + * 显示图像Dialog + * @param context + * @param content uri + */ + public static void showImageDialog(Context context, URL content) { + ImageDialog dialog = new ImageDialog(context, content); + dialog.show(); + } + /** * 显示图像Dialog * @param context @@ -616,6 +699,7 @@ private static class ImageDialog extends Dialog { private Integer id; private String path; private Uri uri; + private URL url; private Bitmap bitmap; private byte[] data; @@ -644,6 +728,11 @@ public ImageDialog(@NonNull Context context, Uri uri) { this.uri = uri; } + public ImageDialog(@NonNull Context context, URL url) { + super(context, R.style.ShadowDialogTheme); + this.url = url; + } + public ImageDialog(@NonNull Context context, byte[] data) { super(context, R.style.ShadowDialogTheme); this.data = data; @@ -697,6 +786,8 @@ public void onClick(View v) { request = manager.load(path); } else if (uri != null) { request = manager.load(uri); + } else if (url != null) { + request = manager.load(url); } else if (bitmap != null){ request = manager.load(bitmap); } else if (data != null){ diff --git a/src/app/src/main/java/com/alipay/hulu/util/FunctionSelectUtil.java b/src/app/src/main/java/com/alipay/hulu/util/FunctionSelectUtil.java index d789664..15c482d 100644 --- a/src/app/src/main/java/com/alipay/hulu/util/FunctionSelectUtil.java +++ b/src/app/src/main/java/com/alipay/hulu/util/FunctionSelectUtil.java @@ -17,32 +17,48 @@ import android.content.Context; import android.content.DialogInterface; -import android.os.Build; -import android.support.v7.app.AlertDialog; -import android.support.v7.widget.AppCompatSpinner; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatSpinner; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; +import android.util.DisplayMetrics; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; +import android.widget.RadioButton; import android.widget.RadioGroup; import android.widget.ScrollView; import android.widget.TextView; -import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatSpinner; + +import com.alibaba.fastjson.JSON; import com.alipay.hulu.R; import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.injector.InjectorService; +import com.alipay.hulu.common.service.ScreenCaptureService; import com.alipay.hulu.common.tools.BackgroundExecutor; +import com.alipay.hulu.common.tools.CmdTools; import com.alipay.hulu.common.utils.ContextUtil; +import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.shared.node.OperationService; import com.alipay.hulu.shared.node.action.Constant; @@ -53,12 +69,15 @@ import com.alipay.hulu.shared.node.action.provider.ActionProviderManager; import com.alipay.hulu.shared.node.action.provider.ViewLoadCallback; import com.alipay.hulu.shared.node.tree.AbstractNodeTree; +import com.alipay.hulu.shared.node.utils.BitmapUtil; import com.alipay.hulu.shared.node.utils.LogicUtil; import com.alipay.hulu.tools.HighLightService; import com.alipay.hulu.ui.CheckableRelativeLayout; import com.alipay.hulu.ui.FlowRadioGroup; +import com.alipay.hulu.ui.GesturePadView; import com.alipay.hulu.ui.TwoLevelSelectLayout; +import java.io.File; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -70,14 +89,16 @@ */ public class FunctionSelectUtil { private static final String TAG = "FunctionSelect"; + public static final String ACTION_EXTRA = "ACTION_EXTRA"; + /** * 展示操作界面 * * @param node */ public static void showFunctionView(final Context context, final AbstractNodeTree node, - final List keys, final List icons, - final Map> secondLevel, + final List keys, final List icons, + final Map> secondLevel, final HighLightService highLightService, final OperationService operationService, final Pair localClickPos, @@ -137,6 +158,11 @@ public void onViewLoaded(View v, Runnable preCall) { final OperationMethod method = new OperationMethod( PerformActionEnum.getActionEnumByCode(action.key)); + // 透传一下 + if (!StringUtil.isEmpty(action.extra)) { + method.putParam(ACTION_EXTRA, action.extra); + } + // 添加控件点击位置 if (localClickPos != null) { method.putParam(OperationExecutor.LOCAL_CLICK_POS_KEY, localClickPos.first + "," + localClickPos.second); @@ -204,45 +230,58 @@ protected static boolean processAction(OperationMethod method, AbstractNodeTree PerformActionEnum action = method.getActionEnum(); if (action == PerformActionEnum.INPUT || action == PerformActionEnum.INPUT_SEARCH - || action == PerformActionEnum.LONG_CLICK) { - showEditView(node, method, context, listener); - return true; - } else if (action == PerformActionEnum.MULTI_CLICK - || action == PerformActionEnum.SLEEP_UNTIL) { - showEditView(node, method, context, listener); - return true; - } else if (action == PerformActionEnum.SLEEP - || action == PerformActionEnum.SCREENSHOT) { - showEditView(null, method, context, listener); - return true; - } else if (action == PerformActionEnum.SCROLL_TO_BOTTOM + || action == PerformActionEnum.CLICK_AND_INPUT + || action == PerformActionEnum.LONG_CLICK + || action == PerformActionEnum.MULTI_CLICK + || action == PerformActionEnum.SLEEP_UNTIL + || action == PerformActionEnum.SLEEP + || action == PerformActionEnum.KEYBOARD_INPUT + || action == PerformActionEnum.INPUT_GLOBAL + || action == PerformActionEnum.SCREENSHOT + || action == PerformActionEnum.SCROLL_TO_BOTTOM || action == PerformActionEnum.SCROLL_TO_TOP || action == PerformActionEnum.SCROLL_TO_LEFT - || action == PerformActionEnum.SCROLL_TO_RIGHT) { + || action == PerformActionEnum.SCROLL_TO_RIGHT + || action == PerformActionEnum.EXECUTE_SHELL) { showEditView(node, method, context, listener); return true; - } else if (action == PerformActionEnum.ASSERT) { - chooseAssertMode(node, PerformActionEnum.ASSERT, context, listener); + } else if (action == PerformActionEnum.ASSERT + || action == PerformActionEnum.ASSERT_TOAST) { + chooseAssertMode(node, action, context, listener); return true; } else if (action == PerformActionEnum.LET_NODE) { - chooseLetMode(node, context, listener); + chooseLetMode(node, context, listener, operationService); + return true; + } else if (action == PerformActionEnum.LET) { + chooseLetGlobalMode(context, listener, operationService); + return true; + } else if (action == PerformActionEnum.CHECK || action == PerformActionEnum.CHECK_NODE) { + chooseCheckMode(node, context, listener, operationService); return true; - } else if (action == PerformActionEnum.JUMP_TO_PAGE) { + } else if (action == PerformActionEnum.JUMP_TO_PAGE + || action == PerformActionEnum.GENERATE_QR_CODE + || action == PerformActionEnum.GENERATE_BAR_CODE + || action == PerformActionEnum.LOAD_PARAM) { showSelectView(method, context, listener); return true; } else if (action == PerformActionEnum.CHANGE_MODE) { showChangeModeView(context, listener); return true; - }else if (action == PerformActionEnum.EXECUTE_SHELL) { - showEditView(node, method, context, listener); - return true; } else if (action == PerformActionEnum.WHILE) { showWhileView(method, context, listener); return true; } else if (action == PerformActionEnum.IF) { method.putParam(LogicUtil.CHECK_PARAM, ""); + } else if (action == PerformActionEnum.GESTURE || action == PerformActionEnum.GLOBAL_GESTURE) { + captureAndShowGesture(action, node, context, listener); + return true; + } else if (action == PerformActionEnum.GLOBAL_SCROLL_TO_BOTTOM + || action == PerformActionEnum.GLOBAL_SCROLL_TO_TOP + || action == PerformActionEnum.GLOBAL_SCROLL_TO_LEFT + || action == PerformActionEnum.GLOBAL_SCROLL_TO_RIGHT) { + showScrollControlView(method, context, listener); + return true; } - return false; } @@ -255,16 +294,11 @@ private static void showChangeModeView(Context context, final FunctionListener l final String[] actions = new String[modes.length]; for (int i = 0; i < modes.length; i++) { - // API21以下不支持截图模式 - if (Build.VERSION.SDK_INT < 21 && modes[i] == RunningModeEnum.CAPTURE_MODE) { - continue; - } - actions[i] = modes[i].getDesc(); } AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.AppDialogTheme) - .setTitle("请选择要切换的模式") + .setTitle(R.string.function__set_mode) .setSingleChoiceItems(actions, 0, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -279,7 +313,7 @@ public void onClick(DialogInterface dialog, int which) { method.putParam(OperationExecutor.GET_NODE_MODE, modes[which].getCode()); listener.onProcessFunction(method, null); } - }).setNegativeButton("取消", new DialogInterface.OnClickListener() { + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); @@ -289,7 +323,7 @@ public void onClick(DialogInterface dialog, int which) { }); AlertDialog dialog = builder.create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); dialog.setCancelable(false); dialog.show(); @@ -300,6 +334,89 @@ public void onClick(DialogInterface dialog, int which) { } } + + /** + * 展示滑动控制 + * @param context + */ + private static void showScrollControlView(final OperationMethod method, Context context, final FunctionListener listener) { + try { + LayoutInflater inflater = LayoutInflater.from(ContextUtil.getContextThemeWrapper( + context, R.style.AppDialogTheme)); + + ScrollView v = (ScrollView) inflater.inflate(R.layout.dialog_setting, null); + LinearLayout view = (LinearLayout) v.getChildAt(0); + + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + // 对每一个字段添加EditText + View editField = inflater.inflate(R.layout.item_edit_field, null); + + final EditText distance = (EditText) editField.findViewById(R.id.item_edit_field_edit); + TextView distanceName = (TextView) editField.findViewById(R.id.item_edit_field_name); + + // 配置字段 + distance.setHint(R.string.scroll_setting__scroll_distense); + distanceName.setText(R.string.scroll_setting__scroll_distense); + distance.setInputType(InputType.TYPE_CLASS_NUMBER); + distance.setText("40"); + + // 设置其他参数 + distance.setTextColor(context.getResources().getColor(R.color.primaryText)); + distance.setHintTextColor(context.getResources().getColor(R.color.secondaryText)); + distance.setHighlightColor(context.getResources().getColor(R.color.colorAccent)); + view.addView(editField, layoutParams); + + + editField = inflater.inflate(R.layout.item_edit_field, null); + final EditText time = (EditText) editField.findViewById(R.id.item_edit_field_edit); + TextView timeName = (TextView) editField.findViewById(R.id.item_edit_field_name); + + // 配置字段 + time.setHint(R.string.scroll_setting__scroll_time); + timeName.setText(R.string.scroll_setting__scroll_time); + time.setText("1000"); + time.setInputType(InputType.TYPE_CLASS_NUMBER); + + // 设置其他参数 + time.setTextColor(context.getResources().getColor(R.color.primaryText)); + time.setHintTextColor(context.getResources().getColor(R.color.secondaryText)); + time.setHighlightColor(context.getResources().getColor(R.color.colorAccent)); + view.addView(editField, layoutParams); + + // 显示Dialog + AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) + .setTitle(R.string.scroll_setting__set_scroll_param) + .setView(v) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // 获取每个编辑框的文字 + dialog.dismiss(); + + method.putParam(OperationExecutor.SCROLL_DISTANCE, distance.getText().toString()); + method.putParam(OperationExecutor.SCROLL_TIME, time.getText().toString()); + listener.onProcessFunction(method, null); + } + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + listener.onCancel(); + } + }).create(); + + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); + dialog.setCanceledOnTouchOutside(false); + dialog.setCancelable(false); + + dialog.show(); + } catch (Exception e) { + LogUtil.e(TAG, "Throw exception: " + e.getMessage(), e); + listener.onCancel(); + } + } + /** * 展示选择框 * @param method @@ -311,25 +428,40 @@ private static void showSelectView(final OperationMethod method, final Context c final PerformActionEnum actionEnum = method.getActionEnum(); View customView = LayoutInflater.from(context).inflate(R.layout.dialog_select_view, null); View itemScan = customView.findViewById(R.id.item_scan); - View itemUrl = customView.findViewById(R.id.item_url); + TextView itemUrl = customView.findViewById(R.id.item_url); + if (actionEnum == PerformActionEnum.GENERATE_QR_CODE + || actionEnum == PerformActionEnum.GENERATE_BAR_CODE) { + itemUrl.setText(R.string.function__input_code); + } final AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) .setView(customView) - .setTitle("请选择操作方式") - .setNegativeButton("取消", new DialogInterface.OnClickListener() { + .setTitle(R.string.function__select_function) + .setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); listener.onCancel(); } }).create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 dialog.setCancelable(false); View.OnClickListener _listener = new View.OnClickListener() { @Override public void onClick(View v) { - if (actionEnum == PerformActionEnum.JUMP_TO_PAGE) { + if (actionEnum == PerformActionEnum.JUMP_TO_PAGE + || actionEnum == PerformActionEnum.GENERATE_QR_CODE + || actionEnum == PerformActionEnum.GENERATE_BAR_CODE) { + if (v.getId() == R.id.item_scan) { + method.putParam("scan", "1"); + listener.onProcessFunction(method, null); + } else if (v.getId() == R.id.item_url) { + dialog.dismiss(); + showUrlEditView(method, context, listener); + return; + } + } else if (actionEnum == PerformActionEnum.LOAD_PARAM) { if (v.getId() == R.id.item_scan) { method.putParam("scan", "1"); listener.onProcessFunction(method, null); @@ -367,12 +499,19 @@ private static void showUrlEditView(final OperationMethod method, Context contex final PerformActionEnum actionEnum = method.getActionEnum(); View v = LayoutInflater.from(ContextUtil.getContextThemeWrapper(context, R.style.AppDialogTheme)).inflate(R.layout.dialog_record_name, null); final EditText edit = (EditText) v.findViewById(R.id.dialog_record_edit); - edit.setHint("请输入url"); + edit.setHint(R.string.function__please_input_url); + if (actionEnum == PerformActionEnum.GENERATE_QR_CODE + || actionEnum == PerformActionEnum.GENERATE_BAR_CODE) { + edit.setHint(R.string.function__please_input_qr_code); + } + + int title = (actionEnum == PerformActionEnum.GENERATE_QR_CODE + || actionEnum == PerformActionEnum.GENERATE_BAR_CODE)? R.string.function__input_qr_code: R.string.function__input_url; AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) - .setTitle("请输入url") + .setTitle(title) .setView(v) - .setPositiveButton("输入", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.function__input, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Positive " + which); @@ -380,14 +519,21 @@ public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); - if (actionEnum == PerformActionEnum.JUMP_TO_PAGE) { + if (actionEnum == PerformActionEnum.JUMP_TO_PAGE + || actionEnum == PerformActionEnum.GENERATE_QR_CODE + || actionEnum == PerformActionEnum.GENERATE_BAR_CODE) { // 向handler发送请求 method.putParam(OperationExecutor.SCHEME_KEY, data); listener.onProcessFunction(method, null); + } else if (actionEnum == PerformActionEnum.LOAD_PARAM) { + method.putParam(OperationExecutor.APP_URL_KEY, data); + + // 向handler发送请求 + listener.onProcessFunction(method, null); } } - }).setNegativeButton("取消", new DialogInterface.OnClickListener() { + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -395,7 +541,7 @@ public void onClick(DialogInterface dialog, int which) { listener.onCancel(); } }).create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 dialog.setCancelable(false); dialog.show(); @@ -408,7 +554,7 @@ public void onClick(DialogInterface dialog, int which) { * @param listener */ private static void chooseLetMode(AbstractNodeTree node, final Context context, - final FunctionListener listener) { + final FunctionListener listener, final OperationService service) { if (node == null) { LogUtil.e(TAG, "Receive null node, can't let value"); @@ -454,6 +600,58 @@ private static void chooseLetMode(AbstractNodeTree node, final Context context, R.id.dialog_action_let_other); final EditText valExpr = (EditText) otherWrapper.findViewById(R.id.dialog_action_let_other_value); final RadioGroup valType = (RadioGroup) otherWrapper.findViewById(R.id.dialog_action_let_other_type); + final TextView valVal = (TextView) otherWrapper.findViewById(R.id.dialog_action_let_other_value_val); + final AbstractNodeTree finalNode = node; + valType.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + if (service != null) { + String expr = valExpr.getText().toString(); + if (StringUtil.isEmpty(expr)) { + valVal.setText(context.getString(R.string.action_let_cur_value, "-")); + return; + } + + String val = LogicUtil.eval(expr, finalNode, checkedId == R.id.dialog_action_let_other_type_int ? + LogicUtil.ALLOC_TYPE_INTEGER : LogicUtil.ALLOC_TYPE_STRING, service); + if (val != null) { + valVal.setText(context.getString(R.string.action_let_cur_value, val)); + } else { + valVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } + } + } + }); + valExpr.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (service != null) { + String expr = s.toString(); + if (StringUtil.isEmpty(expr)) { + valVal.setText(context.getString(R.string.action_let_cur_value, "-")); + return; + } + + String val = LogicUtil.eval(expr, finalNode, valType.getCheckedRadioButtonId() == R.id.dialog_action_let_other_type_int ? + LogicUtil.ALLOC_TYPE_INTEGER : LogicUtil.ALLOC_TYPE_STRING, service); + if (val != null) { + valVal.setText(context.getString(R.string.action_let_cur_value, val)); + } else { + valVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } + } + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); final CheckableRelativeLayout[] previous = {textWrapper}; final String[] valValue = { "${node.text}" }; @@ -502,11 +700,10 @@ public void onCheckedChanged(CheckableRelativeLayout checkable, boolean isChecke final EditText valName = (EditText) letView.findViewById(R.id.dialog_action_let_variable_name); - final AbstractNodeTree finalNode = node; AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) - .setTitle("请设置变量值") + .setTitle(R.string.function__set_variable) .setView(letView) - .setPositiveButton("确定", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Positive " + which); @@ -527,6 +724,327 @@ public void onClick(DialogInterface dialog, int which) { listener.onProcessFunction(method, finalNode); } + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + dialog.dismiss(); + listener.onCancel(); + } + }).create(); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); + dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 + dialog.setCancelable(false); + dialog.show(); + } + + + + /** + * 动态赋值选择框 + * @param context + * @param listener + */ + private static void chooseCheckMode(AbstractNodeTree node, final Context context, + final FunctionListener listener, final OperationService service) { + + // 如果是TextView外面包装的一层,解析内部的TextView + if (node != null) { + if (node.getChildrenNodes() != null && node.getChildrenNodes().size() == 1) { + AbstractNodeTree child = node.getChildrenNodes().get(0); + + if (StringUtil.equals(child.getClassName(), "android.widget.TextView")) { + node = child; + } + } + } + + final AbstractNodeTree finalNode = node; + + // 获取页面 + View checkView = LayoutInflater.from(ContextUtil.getContextThemeWrapper(context, + R.style.AppDialogTheme)).inflate(R.layout.dialog_action_check_global, null); + final EditText leftExpr = (EditText) checkView.findViewById(R.id.dialog_action_check_left_value); + final TextView leftVal = (TextView) checkView.findViewById(R.id.dialog_action_check_left_value_val); + final EditText rightExpr = (EditText) checkView.findViewById(R.id.dialog_action_check_right_value); + final TextView rightVal = (TextView) checkView.findViewById(R.id.dialog_action_check_right_value_val); + + final RadioGroup valType = (RadioGroup) checkView.findViewById(R.id.dialog_action_check_type); + final RadioGroup compareType = (RadioGroup) checkView.findViewById(R.id.dialog_action_check_compare); + + final RadioButton bigger = (RadioButton) compareType.findViewById(R.id.dialog_action_check_compare_bigger); + final RadioButton biggerEqual = (RadioButton) compareType.findViewById(R.id.dialog_action_check_compare_bigger_equal); + final RadioButton less = (RadioButton) compareType.findViewById(R.id.dialog_action_check_compare_less); + final RadioButton lessEqual = (RadioButton) compareType.findViewById(R.id.dialog_action_check_compare_less_equal); + + valType.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + if (service != null) { + String expr = leftExpr.getText().toString(); + if (StringUtil.isEmpty(expr)) { + leftVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } else { + String val = LogicUtil.eval(expr, finalNode, checkedId == R.id.dialog_action_check_type_int ? + LogicUtil.ALLOC_TYPE_INTEGER : LogicUtil.ALLOC_TYPE_STRING, service); + if (val != null) { + leftVal.setText(context.getString(R.string.action_let_cur_value, val)); + } else { + leftVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } + } + + expr = rightExpr.getText().toString(); + if (StringUtil.isEmpty(expr)) { + rightVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } else { + String val = LogicUtil.eval(expr, finalNode, checkedId == R.id.dialog_action_check_type_int ? + LogicUtil.ALLOC_TYPE_INTEGER : LogicUtil.ALLOC_TYPE_STRING, service); + if (val != null) { + rightVal.setText(context.getString(R.string.action_let_cur_value, val)); + } else { + rightVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } + } + } + + if (checkedId == R.id.dialog_action_check_type_str) { + bigger.setEnabled(false); + biggerEqual.setEnabled(false); + less.setEnabled(false); + lessEqual.setEnabled(false); + } else { + bigger.setEnabled(true); + biggerEqual.setEnabled(true); + less.setEnabled(true); + lessEqual.setEnabled(true); + } + + } + }); + leftExpr.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (service != null) { + String expr = s.toString(); + if (StringUtil.isEmpty(expr)) { + leftVal.setText(context.getString(R.string.action_let_cur_value, "-")); + return; + } + + String val = LogicUtil.eval(expr, finalNode, valType.getCheckedRadioButtonId() == R.id.dialog_action_check_type_int ? + LogicUtil.ALLOC_TYPE_INTEGER : LogicUtil.ALLOC_TYPE_STRING, service); + if (val != null) { + leftVal.setText(context.getString(R.string.action_let_cur_value, val)); + } else { + leftVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } + } + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + rightExpr.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (service != null) { + String expr = s.toString(); + if (StringUtil.isEmpty(expr)) { + rightVal.setText(context.getString(R.string.action_let_cur_value, "-")); + return; + } + + String val = LogicUtil.eval(expr, finalNode, valType.getCheckedRadioButtonId() == R.id.dialog_action_check_type_int ? + LogicUtil.ALLOC_TYPE_INTEGER : LogicUtil.ALLOC_TYPE_STRING, service); + if (val != null) { + rightVal.setText(context.getString(R.string.action_let_cur_value, val)); + } else { + rightVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } + } + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) + .setTitle("请设置比较内容") + .setView(checkView) + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + LogUtil.i(TAG, "Positive " + which); + String leftVal = leftExpr.getText().toString(); + String rightVal = rightExpr.getText().toString(); + int targetValType = valType.getCheckedRadioButtonId() == R.id.dialog_action_check_type_int? + LogicUtil.ALLOC_TYPE_INTEGER: LogicUtil.ALLOC_TYPE_STRING; + + dialog.dismiss(); + OperationMethod method; + if (finalNode != null) { + method = new OperationMethod(PerformActionEnum.CHECK_NODE); + } else { + method = new OperationMethod(PerformActionEnum.CHECK); + } + + String connector; + int id = compareType.getCheckedRadioButtonId(); + if (targetValType == LogicUtil.ALLOC_TYPE_INTEGER) { + if (id == R.id.dialog_action_check_compare_equal) { + connector = "=="; + + } else if (id == R.id.dialog_action_check_compare_no_equal) { + connector = "<>"; + + } else if (id == R.id.dialog_action_check_compare_bigger) { + connector = ">"; + + } else if (id == R.id.dialog_action_check_compare_bigger_equal) { + connector = ">="; + + } else if (id == R.id.dialog_action_check_compare_less) { + connector = "<"; + + } else if (id == R.id.dialog_action_check_compare_less_equal) { + connector = "<="; + } else { + LogUtil.w(TAG, "Can't recognize type " + targetValType); + listener.onCancel(); + return; + } + } else { + if (id == R.id.dialog_action_check_compare_equal) { + connector = "="; + + } else if (id == R.id.dialog_action_check_compare_no_equal) { + connector = "!="; + } else { + LogUtil.w(TAG, "Can't recognize type " + targetValType); + listener.onCancel(); + return; + } + } + method.putParam(LogicUtil.CHECK_PARAM, leftVal + connector + rightVal); + listener.onProcessFunction(method, finalNode); + } + }).setNegativeButton("取消", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + dialog.dismiss(); + listener.onCancel(); + } + }).create(); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); + dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 + dialog.setCancelable(false); + dialog.show(); + } + + /** + * 动态赋值选择框 + * @param context + * @param listener + */ + private static void chooseLetGlobalMode(final Context context, + final FunctionListener listener, final OperationService service) { + + // 获取页面 + View letView = LayoutInflater.from(ContextUtil.getContextThemeWrapper(context, + R.style.AppDialogTheme)).inflate(R.layout.dialog_action_let_global, null); + final EditText valExpr = (EditText) letView.findViewById(R.id.dialog_action_let_other_value); + final RadioGroup valType = (RadioGroup) letView.findViewById(R.id.dialog_action_let_other_type); + final TextView valVal = (TextView) letView.findViewById(R.id.dialog_action_let_other_value_val); + valType.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + if (service != null) { + String expr = valExpr.getText().toString(); + if (StringUtil.isEmpty(expr)) { + valVal.setText(context.getString(R.string.action_let_cur_value, "-")); + return; + } + + String val = LogicUtil.eval(expr, null, checkedId == R.id.dialog_action_let_other_type_int ? + LogicUtil.ALLOC_TYPE_INTEGER : LogicUtil.ALLOC_TYPE_STRING, service); + if (val != null) { + valVal.setText(context.getString(R.string.action_let_cur_value, val)); + } else { + valVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } + } + } + }); + valExpr.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + if (service != null) { + String expr = s.toString(); + if (StringUtil.isEmpty(expr)) { + valVal.setText(context.getString(R.string.action_let_cur_value, "-")); + return; + } + + String val = LogicUtil.eval(expr, null, valType.getCheckedRadioButtonId() == R.id.dialog_action_let_other_type_int ? + LogicUtil.ALLOC_TYPE_INTEGER : LogicUtil.ALLOC_TYPE_STRING, service); + if (val != null) { + valVal.setText(context.getString(R.string.action_let_cur_value, val)); + } else { + valVal.setText(context.getString(R.string.action_let_cur_value, "-")); + } + } + } + + @Override + public void afterTextChanged(Editable s) { + + } + }); + + final EditText valName = (EditText) letView.findViewById(R.id.dialog_action_let_variable_name); + + AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) + .setTitle("请设置变量值") + .setView(letView) + .setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + LogUtil.i(TAG, "Positive " + which); + String targetValName = valName.getText().toString(); + String targetValValue = valExpr.getText().toString(); + int targetValType = valType.getCheckedRadioButtonId() == R.id.dialog_action_let_other_type_int? + LogicUtil.ALLOC_TYPE_INTEGER: LogicUtil.ALLOC_TYPE_STRING; + + dialog.dismiss(); + OperationMethod method = new OperationMethod(PerformActionEnum.LET); + method.putParam(LogicUtil.ALLOC_TYPE, Integer.toString(targetValType)); + method.putParam(LogicUtil.ALLOC_VALUE_PARAM, targetValValue); + method.putParam(LogicUtil.ALLOC_KEY_PARAM, targetValName); + + listener.onProcessFunction(method, null); + } }).setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -535,7 +1053,7 @@ public void onClick(DialogInterface dialog, int which) { listener.onCancel(); } }).create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 dialog.setCancelable(false); dialog.show(); @@ -557,10 +1075,14 @@ private static void chooseAssertMode(final AbstractNodeTree node, final PerformA // 判断当前内容是否是数字 StringBuilder matchTxtBuilder = new StringBuilder(); - for (AbstractNodeTree item : node) { - if (!TextUtils.isEmpty(item.getText())) { - matchTxtBuilder.append(item.getText()); + if (action == PerformActionEnum.ASSERT) { + for (AbstractNodeTree item : node) { + if (!TextUtils.isEmpty(item.getText())) { + matchTxtBuilder.append(item.getText()); + } } + } else if (action == PerformActionEnum.ASSERT_TOAST) { + matchTxtBuilder.append(InjectorService.g().getMessage(com.alipay.hulu.shared.event.constant.Constant.EVENT_TOAST_MSG, String.class)); } final int[] selectNumIndex = new int[1]; @@ -575,22 +1097,16 @@ private static void chooseAssertMode(final AbstractNodeTree node, final PerformA assertGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { - switch (checkedId) { - case R.id.ch1: - selectNumIndex[0] = 0; - break; - case R.id.ch2: - selectNumIndex[0] = 1; - break; - case R.id.ch3: - selectNumIndex[0] = 2; - break; - case R.id.ch4: - selectNumIndex[0] = 3; - break; - case R.id.ch5: - selectNumIndex[0] = 4; - break; + if (checkedId == R.id.ch1) { + selectNumIndex[0] = 0; + } else if (checkedId == R.id.ch2) { + selectNumIndex[0] = 1; + } else if (checkedId == R.id.ch3) { + selectNumIndex[0] = 2; + } else if (checkedId == R.id.ch4) { + selectNumIndex[0] = 3; + } else if (checkedId == R.id.ch5) { + selectNumIndex[0] = 4; } } }); @@ -616,10 +1132,10 @@ public void afterTextChanged(Editable s) { }); AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) - .setTitle("输入数字并选择断言模式") + .setTitle(R.string.function__input_number_assert) .setView(content) - .setPositiveButton("确定", null) - .setNegativeButton("取消", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.constant__confirm, null) + .setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Negative " + which); @@ -640,13 +1156,13 @@ public void onClick(View v) { param.put(OperationExecutor.ASSERT_INPUT_CONTENT, strResult[0]); postiveClick(action, node, dialog, param, listener); } else { - Toast.makeText(context, "请输入数字", Toast.LENGTH_SHORT).show(); + LauncherApplication.toast(R.string.function__input_number); } } }); } }); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); dialog.setCancelable(false); dialog.show(); @@ -664,16 +1180,12 @@ public void onClick(View v) { assertGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { - switch (checkedId) { - case R.id.ch1: - selectIndex[0] = 0; - break; - case R.id.ch2: - selectIndex[0] = 1; - break; - case R.id.ch3: - selectIndex[0] = 2; - break; + if (checkedId == R.id.ch1) { + selectIndex[0] = 0; + } else if (checkedId == R.id.ch2) { + selectIndex[0] = 1; + } else if (checkedId == R.id.ch3) { + selectIndex[0] = 2; } } }); @@ -696,10 +1208,10 @@ public void afterTextChanged(Editable s) { }); AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) - .setTitle("输入内容并选择断言模式") + .setTitle(R.string.function__input_select_assert) .setView(content) - .setPositiveButton("确定", null) - .setNegativeButton("取消", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.constant__confirm, null) + .setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Negative " + which); @@ -724,13 +1236,13 @@ public void onClick(View v) { param, listener); } else { - Toast.makeText(context, "请输入内容", Toast.LENGTH_SHORT).show(); + LauncherApplication.toast(R.string.function__input_content); } } }); } }); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); dialog.setCancelable(false); dialog.show(); @@ -771,58 +1283,67 @@ public void run() { * * @param node */ - protected static void showEditView(final AbstractNodeTree node, final OperationMethod method, + public static void showEditView(final AbstractNodeTree node, final OperationMethod method, final Context context, final FunctionListener listener) { try { PerformActionEnum action = method.getActionEnum(); - String title = "请输入具体内容"; + String title = StringUtil.getString(R.string.function__input_title); View v = LayoutInflater.from(ContextUtil.getContextThemeWrapper(context, R.style.AppDialogTheme)).inflate(R.layout.dialog_record_name, null); final EditText edit = (EditText) v.findViewById(R.id.dialog_record_edit); + View hide = v.findViewById(R.id.dialog_record_edit_hide); + hide.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideInput(edit); + } + }); final Pattern textPattern; if (action == PerformActionEnum.SLEEP) { - edit.setHint("sleep时长(单位ms)"); - title = "请设置Sleep时长"; + edit.setHint(R.string.function__sleep_time); + title = StringUtil.getString(R.string.function__set_sleep_time); textPattern = Pattern.compile("\\d+"); } else if (action == PerformActionEnum.SCREENSHOT) { - edit.setHint("截图名称"); - title = "请输入截图名称"; + edit.setHint(R.string.function__screenshot_name); + title = StringUtil.getString(R.string.function__set_screenshot_name); textPattern = Pattern.compile("\\S+(.*\\S+)?"); } else if (action ==PerformActionEnum.MULTI_CLICK) { - edit.setHint("点击次数"); - title = "请输入连续点击次数(1-99次)"; + edit.setHint(R.string.function__click_time); + title = StringUtil.getString(R.string.function__set_click_time); textPattern = Pattern.compile("\\d{1,2}"); } else if (action ==PerformActionEnum.SLEEP_UNTIL) { - edit.setHint("最长等待时间"); + edit.setHint(R.string.function__max_wait); edit.setText(R.string.default_sleep_time); - title = "请输入最长等待时间(单位ms)"; + title = StringUtil.getString(R.string.function__set_max_wait); textPattern = Pattern.compile("\\d+"); } else if (action == PerformActionEnum.SCROLL_TO_BOTTOM || action == PerformActionEnum.SCROLL_TO_TOP || action == PerformActionEnum.SCROLL_TO_LEFT || action == PerformActionEnum.SCROLL_TO_RIGHT) { - edit.setHint("滑动百分比"); + edit.setHint(R.string.function__scroll_percent); edit.setText(R.string.default_scroll_percentage); - title = "请输入滑动百分比"; + title = StringUtil.getString(R.string.function__set_scroll_percent); textPattern = Pattern.compile("\\d+"); } else if (action == PerformActionEnum.EXECUTE_SHELL) { - edit.setHint("请输入adb shell命令"); - title = "请输入shell命令"; + edit.setHint(R.string.function__adb_cmd); + title = StringUtil.getString(R.string.function__set_adb_cmd); textPattern = null; + } else if (action == PerformActionEnum.KEYBOARD_INPUT) { + textPattern = Pattern.compile("[a-zA-Z0-9]+"); } else if (action == PerformActionEnum.LONG_CLICK) { - edit.setHint("长按时长"); - title = "请输入长按时长(单位ms)"; + edit.setHint(R.string.function__long_press); + title = StringUtil.getString(R.string.function__set_long_press); textPattern = Pattern.compile("[1-9]\\d+"); edit.setText(R.string.default_long_click_time); } else { - edit.setHint("具体内容"); + edit.setHint(R.string.function__input_content); textPattern = null; } final AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) .setTitle(title) .setView(v) - .setPositiveButton("输入", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.function__input, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Positive " + which); @@ -843,7 +1364,7 @@ public void run() { } }, 500); } - }).setNegativeButton("取消", new DialogInterface.OnClickListener() { + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Negative " + which); @@ -853,7 +1374,7 @@ public void onClick(DialogInterface dialog, int which) { } }).create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 dialog.setCancelable(false); dialog.show(); @@ -890,10 +1411,19 @@ public void afterTextChanged(Editable s) { } catch (Exception e) { LogUtil.e(TAG, "Throw exception: " + e.getMessage(), e); - + listener.onCancel(); } } + /** + * 隐藏输入法 + * @param editText + */ + private static void hideInput(EditText editText) { + InputMethodManager inputMethodManager = (InputMethodManager) editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + inputMethodManager.hideSoftInputFromWindow(editText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + /** * 展示WHILE编辑界面 * @param method @@ -914,12 +1444,12 @@ public void onItemSelected(AdapterView parent, View view, int position, long edit.clearComposingText(); if (position == 0) { - hint.setText("循环次数"); - edit.setHint("循环次数"); + hint.setText(R.string.function__loop_count); + edit.setHint(R.string.function__loop_count); edit.setInputType(InputType.TYPE_CLASS_NUMBER); } else { - hint.setText("循环条件"); - edit.setHint("循环条件"); + hint.setText(R.string.function__loop_condition); + edit.setHint(R.string.function__loop_condition); edit.setInputType(InputType.TYPE_CLASS_TEXT); } } @@ -931,9 +1461,9 @@ public void onNothingSelected(AdapterView parent) { }); final AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) - .setTitle("添加循环") + .setTitle(R.string.function__add_loop) .setView(v) - .setPositiveButton("添加", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.function__add, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Positive " + which); @@ -958,7 +1488,7 @@ public void run() { } }, 500); } - }).setNegativeButton("取消", new DialogInterface.OnClickListener() { + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Negative " + which); @@ -968,7 +1498,7 @@ public void onClick(DialogInterface dialog, int which) { } }).create(); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 dialog.setCancelable(false); dialog.show(); @@ -999,7 +1529,7 @@ private static void showProvidedView(final AbstractNodeTree node, final Operatio AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) .setView(view) .setCancelable(false) - .setPositiveButton("确定", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Positive " + which); @@ -1020,7 +1550,7 @@ public void run() { listener.onProcessFunction(method, node); } } - }).setNegativeButton("取消", new DialogInterface.OnClickListener() { + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Negative " + which); @@ -1037,10 +1567,141 @@ public void onClick(DialogInterface dialog, int which) { dialog.setTitle(null); dialog.setCanceledOnTouchOutside(false); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); dialog.show(); } + /** + * 展示登录信息框 + * @param action + * @param context + */ + private static void captureAndShowGesture(final PerformActionEnum action, final AbstractNodeTree target, Context context, final FunctionListener listener) { + try { + View v = LayoutInflater.from(ContextUtil.getContextThemeWrapper(context, R.style.AppDialogTheme)).inflate(R.layout.dialog_node_gesture, null); + final GesturePadView gesturePadView = (GesturePadView) v.findViewById(R.id.node_gesture_gesture_view); + final RadioGroup group = (RadioGroup) v.findViewById(R.id.node_gesture_time_filter); + group.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + int targetTime; + if (checkedId == R.id.node_gesture_time_filter_25) { + targetTime = 25; + } else if (checkedId == R.id.node_gesture_time_filter_50) { + targetTime = 50; + } else if (checkedId == R.id.node_gesture_time_filter_200) { + targetTime = 200; + } else { + targetTime = 100; + } + gesturePadView.setGestureFilter(targetTime); + gesturePadView.clear(); + } + }); + Bitmap nodeBitmap; + if (target != null) { + String capture = target.getCapture(); + if (StringUtil.isEmpty(capture)) { + File captureFile = new File(FileUtils.getSubDir("tmp"), "test.jpg"); + Bitmap bitmap = capture(captureFile); + if (bitmap == null) { + LauncherApplication.getInstance().showToast(context.getString(R.string.action_gesture__capture_screen_failed)); + listener.onCancel(); + return; + } + + Rect displayRect = target.getNodeBound(); + + nodeBitmap = Bitmap.createBitmap(bitmap, displayRect.left, + displayRect.top, displayRect.width(), + displayRect.height()); + target.setCapture(BitmapUtil.bitmapToBase64(nodeBitmap)); + } else { + nodeBitmap = BitmapUtil.base64ToBitmap(capture); + } + } else { + File captureFile = new File(FileUtils.getSubDir("tmp"), "test.jpg"); + nodeBitmap = capture(captureFile); + if (nodeBitmap == null) { + LauncherApplication.getInstance().showToast(context.getString(R.string.action_gesture__capture_screen_failed)); + listener.onCancel(); + return; + } + } + + gesturePadView.setTargetImage(new BitmapDrawable(nodeBitmap)); + + AlertDialog dialog = new AlertDialog.Builder(context, R.style.AppDialogTheme) + .setTitle(R.string.gesture__please_record_gesture) + .setView(v) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + LogUtil.i(TAG, "Positive " + which); + List path = gesturePadView.getGesturePath(); + int gestureFilter = gesturePadView.getGestureFilter(); + // 拼装参数 + // 拼装参数 + OperationMethod method = new OperationMethod(action); + method.putParam(OperationExecutor.GESTURE_PATH, JSON.toJSONString(path)); + method.putParam(OperationExecutor.GESTURE_FILTER, Integer.toString(gestureFilter)); + + // 隐藏Dialog + dialog.dismiss(); + + listener.onProcessFunction(method, target); + } + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + LogUtil.i(TAG, "Negative " + which); + + dialog.dismiss(); + listener.onCancel(); + } + }).create(); + dialog.getWindow().setType(com.alipay.hulu.common.constant.Constant.TYPE_ALERT); + dialog.setCanceledOnTouchOutside(false); //点击外面区域不会让dialog消失 + dialog.setCancelable(false); + dialog.show(); + + dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); + + } catch (Exception e) { + LogUtil.e(TAG, "Login info dialog throw exception: " + e.getMessage(), e); + listener.onCancel(); + } + } + + /** + * 截图 + * @param captureFile 截图保留文件 + * @return + */ + private static Bitmap capture(File captureFile) { + DisplayMetrics metrics = new DisplayMetrics(); + ((WindowManager) LauncherApplication.getInstance().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRealMetrics(metrics); + + ScreenCaptureService captureService = LauncherApplication.service(ScreenCaptureService.class); + Bitmap bitmap = null; + if (captureService != null) { + bitmap = captureService.captureScreen(captureFile, metrics.widthPixels, metrics.heightPixels, + metrics.widthPixels, metrics.heightPixels); + } + // 原有截图方案失败 + if (bitmap == null) { + String path = FileUtils.getPathInShell(captureFile); + CmdTools.execHighPrivilegeCmd("screencap -p \"" + path + "\""); + MiscUtil.sleep(1000); + bitmap = BitmapFactory.decodeFile(captureFile.getPath()); + // 长宽不对 + if (bitmap != null && bitmap.getWidth() != metrics.widthPixels) { + bitmap = Bitmap.createScaledBitmap(bitmap, metrics.widthPixels, metrics.heightPixels, false); + } + } + return bitmap; + } + /** * 回调 */ diff --git a/src/app/src/main/java/com/alipay/hulu/util/RecordUtil.java b/src/app/src/main/java/com/alipay/hulu/util/RecordUtil.java index acaa57a..216936f 100644 --- a/src/app/src/main/java/com/alipay/hulu/util/RecordUtil.java +++ b/src/app/src/main/java/com/alipay/hulu/util/RecordUtil.java @@ -51,6 +51,7 @@ */ public class RecordUtil { private static final String TAG = "RecordUtil"; + private static final DateFormat TIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA); /** * 保存到文件夹 @@ -92,12 +93,13 @@ public static File saveToFile(Map> BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(saveFile), charset)); // 第一行写标题 - writer.write("RecordTime," + pattern.getName() + "(" + pattern.getUnit() + "),extra\n"); + writer.write("RecordTime," + pattern.getName() + "(" + pattern.getUnit() + "),extra,SimpleTime\n"); writer.flush(); + long dataStartTime = entry.getKey().getStartTime(); // 写入录制 for (RecordPattern.RecordItem item: entry.getValue()) { - writer.write(item.time + "," + item.value + "," + item.extra + "\n"); + writer.write(item.time + "," + item.value + "," + item.extra + "," + (item.time - dataStartTime) / 1000F + "\n"); writer.flush(); } writer.close(); @@ -121,8 +123,7 @@ public static File saveToFile(Map> */ private static File loadSaveDir(Date startTime, Date endTime) { File recordDir = FileUtils.getSubDir("records"); - DateFormat format = new SimpleDateFormat("MM月dd日HH:mm:ss", Locale.CHINA); - File saveFolder = new File(recordDir, format.format(startTime) + "-" + format.format(endTime)); + File saveFolder = new File(recordDir, TIME_FORMAT.format(startTime) + "_" + TIME_FORMAT.format(endTime)); saveFolder.mkdir(); return saveFolder; } diff --git a/src/app/src/main/java/com/alipay/hulu/util/SystemUtil.java b/src/app/src/main/java/com/alipay/hulu/util/SystemUtil.java index 1ea360a..bc716e3 100644 --- a/src/app/src/main/java/com/alipay/hulu/util/SystemUtil.java +++ b/src/app/src/main/java/com/alipay/hulu/util/SystemUtil.java @@ -24,20 +24,23 @@ * Created by lezhou.wyl on 2018/1/21. */ public class SystemUtil { + public static int VERSION_CODE = BuildConfig.VERSION_CODE; + public static String VERSION_NAME = BuildConfig.VERSION_NAME; + private static final String TAG = "SystemUtil"; public static boolean isUiThread() { - return Looper.getMainLooper().getThread() == Thread.currentThread(); + return Looper.myLooper() == Looper.getMainLooper(); } private static final String CURRENT_PACKAGE_NAME = LauncherApplication.getInstance().getPackageName(); public static int getAppVersionCode() { - return BuildConfig.VERSION_CODE; + return VERSION_CODE; } public static String getAppVersionName() { - return BuildConfig.VERSION_NAME; + return VERSION_NAME; } } diff --git a/src/app/src/main/res/drawable/accent_button_background.xml b/src/app/src/main/res/drawable/accent_button_background.xml new file mode 100644 index 0000000..c3860d2 --- /dev/null +++ b/src/app/src/main/res/drawable/accent_button_background.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable/accent_button_layer.xml b/src/app/src/main/res/drawable/accent_button_layer.xml new file mode 100644 index 0000000..3f27f22 --- /dev/null +++ b/src/app/src/main/res/drawable/accent_button_layer.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable/bg_template_list.xml b/src/app/src/main/res/drawable/bg_template_list.xml new file mode 100644 index 0000000..42ab304 --- /dev/null +++ b/src/app/src/main/res/drawable/bg_template_list.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable/case_edit.xml b/src/app/src/main/res/drawable/case_edit.xml index 98b3f0d..1e6c6e3 100644 --- a/src/app/src/main/res/drawable/case_edit.xml +++ b/src/app/src/main/res/drawable/case_edit.xml @@ -1,4 +1,18 @@ - + + + + diff --git a/src/app/src/main/res/drawable/case_result_item_toggle.xml b/src/app/src/main/res/drawable/case_result_item_toggle.xml index c4c06ee..4229056 100644 --- a/src/app/src/main/res/drawable/case_result_item_toggle.xml +++ b/src/app/src/main/res/drawable/case_result_item_toggle.xml @@ -1,3 +1,18 @@ + diff --git a/src/app/src/main/res/drawable/case_step_copy.xml b/src/app/src/main/res/drawable/case_step_copy.xml new file mode 100644 index 0000000..420550c --- /dev/null +++ b/src/app/src/main/res/drawable/case_step_copy.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/src/app/src/main/res/drawable/case_step_insert_icon.xml b/src/app/src/main/res/drawable/case_step_insert_icon.xml new file mode 100644 index 0000000..93b4260 --- /dev/null +++ b/src/app/src/main/res/drawable/case_step_insert_icon.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/src/app/src/main/res/drawable/case_step_paste.xml b/src/app/src/main/res/drawable/case_step_paste.xml new file mode 100644 index 0000000..f53f315 --- /dev/null +++ b/src/app/src/main/res/drawable/case_step_paste.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/src/app/src/main/res/drawable/case_step_select.xml b/src/app/src/main/res/drawable/case_step_select.xml new file mode 100644 index 0000000..49e5e41 --- /dev/null +++ b/src/app/src/main/res/drawable/case_step_select.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/src/app/src/main/res/drawable/close_icon.xml b/src/app/src/main/res/drawable/close_icon.xml index 07c0d73..2e095cc 100644 --- a/src/app/src/main/res/drawable/close_icon.xml +++ b/src/app/src/main/res/drawable/close_icon.xml @@ -1,3 +1,18 @@ + + + + + + + + diff --git a/src/app/src/main/res/drawable/icon_batch_play.xml b/src/app/src/main/res/drawable/icon_batch_play.xml new file mode 100644 index 0000000..ef2dede --- /dev/null +++ b/src/app/src/main/res/drawable/icon_batch_play.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/src/app/src/main/res/drawable/icon_config.xml b/src/app/src/main/res/drawable/icon_config.xml index b40f185..c41ba23 100644 --- a/src/app/src/main/res/drawable/icon_config.xml +++ b/src/app/src/main/res/drawable/icon_config.xml @@ -1,4 +1,18 @@ - + + + + + + diff --git a/src/app/src/main/res/drawable/icon_remote_connect.xml b/src/app/src/main/res/drawable/icon_remote_connect.xml index 33987c7..4c7bc4d 100644 --- a/src/app/src/main/res/drawable/icon_remote_connect.xml +++ b/src/app/src/main/res/drawable/icon_remote_connect.xml @@ -1,4 +1,18 @@ - + + + + + + + diff --git a/src/app/src/main/res/drawable/icon_scan.xml b/src/app/src/main/res/drawable/icon_scan.xml new file mode 100644 index 0000000..544fd02 --- /dev/null +++ b/src/app/src/main/res/drawable/icon_scan.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable-xhdpi/icon_xingneng.png b/src/app/src/main/res/drawable/icon_xingneng.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/icon_xingneng.png rename to src/app/src/main/res/drawable/icon_xingneng.png diff --git a/src/app/src/main/res/drawable-xhdpi/icon_yijiduokong.png b/src/app/src/main/res/drawable/icon_yijiduokong.png similarity index 100% rename from src/app/src/main/res/drawable-xhdpi/icon_yijiduokong.png rename to src/app/src/main/res/drawable/icon_yijiduokong.png diff --git a/src/app/src/main/res/drawable/item_scan.xml b/src/app/src/main/res/drawable/item_scan.xml index 8c95f2f..619a8e6 100644 --- a/src/app/src/main/res/drawable/item_scan.xml +++ b/src/app/src/main/res/drawable/item_scan.xml @@ -1,3 +1,18 @@ + + + + + diff --git a/src/app/src/main/res/drawable/slim_divider.xml b/src/app/src/main/res/drawable/slim_divider.xml new file mode 100644 index 0000000..f2e0e2c --- /dev/null +++ b/src/app/src/main/res/drawable/slim_divider.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable/solopi_float.xml b/src/app/src/main/res/drawable/solopi_float.xml index 424c849..0010a7c 100644 --- a/src/app/src/main/res/drawable/solopi_float.xml +++ b/src/app/src/main/res/drawable/solopi_float.xml @@ -1,4 +1,18 @@ - + + + + + + android:width="@dimen/control_dp12" + android:height="@dimen/control_dp12" /> \ No newline at end of file diff --git a/src/app/src/main/res/layout-land/activity_record_chart.xml b/src/app/src/main/res/layout-land/activity_record_chart.xml new file mode 100644 index 0000000..f2b520c --- /dev/null +++ b/src/app/src/main/res/layout-land/activity_record_chart.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/layout-v23/item_tools_grid.xml b/src/app/src/main/res/layout-v23/item_tools_grid.xml index 912073b..536ade3 100644 --- a/src/app/src/main/res/layout-v23/item_tools_grid.xml +++ b/src/app/src/main/res/layout-v23/item_tools_grid.xml @@ -15,36 +15,36 @@ --> + android:textSize="@dimen/textsize_10" /> + android:layout_marginTop="@dimen/control_dp30" + /> + android:layout_marginTop="@dimen/control_dp12" + android:textSize="@dimen/textsize_13" /> \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_batch_execution.xml b/src/app/src/main/res/layout/activity_batch_execution.xml index 8168ff0..05768f6 100644 --- a/src/app/src/main/res/layout/activity_batch_execution.xml +++ b/src/app/src/main/res/layout/activity_batch_execution.xml @@ -22,22 +22,29 @@ android:orientation="vertical"> - + android:layout_height="@dimen/control_dp48" + app:tabGravity="fill" + app:tabTextAppearance="@style/TabLayoutTextStyle" + app:tabPaddingTop="@dimen/control_dp8" + app:tabPaddingBottom="@dimen/control_dp8" + app:tabPaddingStart="@dimen/control_dp16" + app:tabPaddingEnd="@dimen/control_dp16" + app:tabMode="fixed" + app:tabMaxWidth="0dp"/> - - + + android:layout_height="@dimen/batch_execute__scroll_height"> + android:paddingTop="@dimen/control_dp8" + android:paddingBottom="@dimen/control_dp8" + android:paddingLeft="@dimen/control_dp8" + android:paddingRight="@dimen/control_dp8"> @@ -77,15 +84,16 @@ + android:paddingTop="@dimen/control_dp8" + android:paddingBottom="@dimen/control_dp8" + android:paddingLeft="@dimen/control_dp16" + android:paddingRight="@dimen/control_dp16"> @@ -95,9 +103,12 @@ android:layout_centerVertical="true" android:id="@+id/batch_execute_start_btn" android:layout_width="wrap_content" - android:layout_height="30dp" + android:paddingLeft="@dimen/control_dp12" + android:paddingRight="@dimen/control_dp12" + android:layout_height="@dimen/control_dp30" android:background="@drawable/bg_confirm_btn" android:text="@string/constant__start_execution" + android:textSize="@dimen/textsize_14" android:textColor="@color/white"/> diff --git a/src/app/src/main/res/layout/activity_batch_replay_result.xml b/src/app/src/main/res/layout/activity_batch_replay_result.xml index a8d32cc..b581755 100644 --- a/src/app/src/main/res/layout/activity_batch_replay_result.xml +++ b/src/app/src/main/res/layout/activity_batch_replay_result.xml @@ -24,45 +24,42 @@ + android:layout_marginTop="@dimen/control_dp12" + android:textSize="@dimen/textsize_20"/> + android:textSize="@dimen/textsize_14"/> + android:textSize="@dimen/textsize_14"/> + android:textSize="@dimen/textsize_14"/> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_choose_layout.xml b/src/app/src/main/res/layout/activity_choose_layout.xml index bab20a5..3e9a179 100644 --- a/src/app/src/main/res/layout/activity_choose_layout.xml +++ b/src/app/src/main/res/layout/activity_choose_layout.xml @@ -15,14 +15,14 @@ --> + android:paddingStart="@dimen/control_dp8" + android:paddingEnd="@dimen/control_dp8" + android:paddingTop="@dimen/control_dp4" + android:paddingBottom="@dimen/control_dp4"> @@ -39,7 +39,7 @@ android:layout_height="wrap_content" android:maxLines="1" android:textColor="@color/black" - android:textSize="20dp" + android:textSize="@dimen/textsize_20" android:layout_gravity="start" android:id="@+id/choose_title"/> \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_display_replay_result.xml b/src/app/src/main/res/layout/activity_display_replay_result.xml index 6b0b4c8..fe9e699 100644 --- a/src/app/src/main/res/layout/activity_display_replay_result.xml +++ b/src/app/src/main/res/layout/activity_display_replay_result.xml @@ -23,13 +23,13 @@ + android:padding="@dimen/control_dp16"> @@ -37,53 +37,60 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/case_name" - android:layout_marginTop="4dp" + android:layout_marginTop="@dimen/control_dp4" android:orientation="vertical"> - + android:layout_height="@dimen/control_dp48" + app:tabTextAppearance="@style/TabLayoutTextStyle" + app:tabPaddingTop="@dimen/control_dp8" + app:tabPaddingBottom="@dimen/control_dp8" + app:tabPaddingStart="@dimen/control_dp8" + app:tabPaddingEnd="@dimen/control_dp8" + app:tabGravity="fill" + app:tabMode="fixed" + app:tabMaxWidth="0dp"/> - - + \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_edit_case.xml b/src/app/src/main/res/layout/activity_edit_case.xml index 2e1f642..66455fc 100644 --- a/src/app/src/main/res/layout/activity_edit_case.xml +++ b/src/app/src/main/res/layout/activity_edit_case.xml @@ -20,19 +20,26 @@ android:layout_height="match_parent"> - + android:layout_height="@dimen/control_dp48" + app:tabTextAppearance="@style/TabLayoutTextStyle" + app:tabPaddingTop="@dimen/control_dp8" + app:tabPaddingBottom="@dimen/control_dp8" + app:tabPaddingStart="@dimen/control_dp16" + app:tabPaddingEnd="@dimen/control_dp16" + app:tabGravity="fill" + app:tabMode="fixed" + app:tabMaxWidth="0dp"/> - - + \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_index.xml b/src/app/src/main/res/layout/activity_index.xml index c0cecaa..e45ed3d 100644 --- a/src/app/src/main/res/layout/activity_index.xml +++ b/src/app/src/main/res/layout/activity_index.xml @@ -27,12 +27,12 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" - android:horizontalSpacing="20dp" + android:horizontalSpacing="@dimen/control_dp20" android:numColumns="2" - android:padding="10dp" + android:padding="@dimen/control_dp12" android:clipToPadding="false" android:scrollbars="none" android:verticalScrollbarPosition="right" - android:verticalSpacing="10dp" + android:verticalSpacing="@dimen/control_dp12" android:overScrollMode="never"/> \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_info.xml b/src/app/src/main/res/layout/activity_info.xml index c9a29cc..014a24e 100644 --- a/src/app/src/main/res/layout/activity_info.xml +++ b/src/app/src/main/res/layout/activity_info.xml @@ -28,18 +28,18 @@ android:orientation="vertical"> @@ -50,16 +50,16 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:textColor="@color/secondaryText" - android:textSize="14sp" - android:layout_marginTop="2dp"/> + android:textSize="@dimen/textsize_14" + android:layout_marginTop="@dimen/control_dp2"/> @@ -78,9 +79,9 @@ android:id="@+id/info_license_text" android:layout_width="match_parent" android:foreground="?attr/selectableItemBackground" - android:layout_height="44dp" - android:paddingLeft="16dp" - android:paddingRight="8dp" + android:layout_height="@dimen/control_dp40" + android:paddingLeft="@dimen/control_dp16" + android:paddingRight="@dimen/control_dp8" android:gravity="center_vertical" android:orientation="horizontal"> @@ -88,18 +89,18 @@ android:id="@+id/title" android:layout_height="wrap_content" android:layout_width="0dp" - android:textSize="19dp" + android:textSize="@dimen/textsize_18" android:textColor="@color/primaryText" android:layout_gravity="center_vertical" - android:text="版权信息" + android:text="@string/info__license_info" android:layout_weight="1" /> - + android:layout_height="@dimen/control_dp48" + app:tabTextAppearance="@style/TabLayoutTextStyle" + app:tabPaddingTop="@dimen/control_dp8" + app:tabPaddingBottom="@dimen/control_dp8" + app:tabPaddingStart="@dimen/control_dp16" + app:tabPaddingEnd="@dimen/control_dp16" + app:tabGravity="fill" + app:tabIndicatorHeight="@dimen/control_dp2" + app:tabMode="fixed"/> - - + \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_patch_status.xml b/src/app/src/main/res/layout/activity_patch_status.xml new file mode 100644 index 0000000..62f9049 --- /dev/null +++ b/src/app/src/main/res/layout/activity_patch_status.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_performance.xml b/src/app/src/main/res/layout/activity_performance.xml index 8b1ef7f..b902b5d 100644 --- a/src/app/src/main/res/layout/activity_performance.xml +++ b/src/app/src/main/res/layout/activity_performance.xml @@ -33,16 +33,16 @@ android:background="@color/white"> @@ -57,10 +57,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/column_app" - android:padding="5dp" + android:padding="@dimen/control_dp6" android:layout_gravity="center_vertical" > - @@ -90,7 +90,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingLeft="16dp"> + android:paddingLeft="@dimen/control_dp16"> @@ -128,16 +128,16 @@ @@ -146,7 +146,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:paddingLeft="16dp"> + android:paddingLeft="@dimen/control_dp16"> diff --git a/src/app/src/main/res/layout/activity_record_chart.xml b/src/app/src/main/res/layout/activity_record_chart.xml index 28218f8..11cf2d3 100644 --- a/src/app/src/main/res/layout/activity_record_chart.xml +++ b/src/app/src/main/res/layout/activity_record_chart.xml @@ -22,88 +22,70 @@ android:id="@+id/head_layout" layout="@layout/head_panel_layout"/> - - - - + android:layout_height="@dimen/control_dp30" + android:text="@string/record_chart__select_data" + android:textSize="@dimen/textsize_14" + android:paddingLeft="@dimen/control_dp8" + android:paddingBottom="@dimen/control_dp2" + android:gravity="bottom" + android:textColor="#a3a3a3"/> - - - + android:layout_height="@dimen/dp_44" + android:id="@+id/record_spinner"/> - - - + android:layout_height="@dimen/dp_44"/> - - - + android:background="@color/default_background_color" + android:text="@string/record_chart__data_display" + android:textSize="@dimen/textsize_14" + android:gravity="bottom" + android:paddingLeft="@dimen/control_dp8" + android:paddingBottom="@dimen/control_dp2" + android:layout_gravity="bottom" + android:textColor="#a3a3a3"/> @@ -112,5 +94,4 @@ android:layout_height="match_parent" android:id="@+id/record_chart"/> - \ No newline at end of file diff --git a/src/app/src/main/res/layout/activity_record_config.xml b/src/app/src/main/res/layout/activity_record_config.xml index e29281b..e5ef7db 100644 --- a/src/app/src/main/res/layout/activity_record_config.xml +++ b/src/app/src/main/res/layout/activity_record_config.xml @@ -26,74 +26,77 @@ + android:layout_marginLeft="@dimen/control_dp16" + android:layout_marginRight="@dimen/control_dp16" + android:layout_marginTop="@dimen/control_dp16" + android:textSize="@dimen/textsize_16" + android:text="@string/record_config__title"/>