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 d0696cb..69cce70 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -176,17 +176,6 @@ greenDAO binaries and source code can be used according to the Apache License, V > Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0 -## EventBus - -> https://github.com/greenrobot/EventBus - - -Copyright (C) 2012-2017 Markus Junginger, greenrobot (http://greenrobot.org) - -EventBus 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 @@ -261,6 +250,16 @@ 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 @@ -445,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 e62750d..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分支 ### 功能特性 @@ -11,31 +14,33 @@ ![录制回放](assets/replay.gif) -**[游戏录制回放使用视频](videos/游戏自动化.mp4)** +**[游戏录制回放使用视频](https://gw.alipayobjects.com/mdn/rms_e29b5f/afts/file/A*ym07T6nACDIAAAAAAAAAAABkARQnAQ)** + +**[Native应用录制回放使用视频](https://gw.alipayobjects.com/os/basement_prod/3472d35c-bd57-4c82-8112-5dcde42fcb32.mov)** -**[Native应用录制回放使用视频](videos/垃圾邮件处理.mov)** +SoloPi拥有录制操作的能力,用户只需要通过SoloPi执行用例步骤,SoloPi就能够将用户的操作记录下来,并且支持在各个设备上进行回放,这一切都能够在手机上独立完成。详见[录制回放](../../wikis/RecordCase)一篇。 -Soloπ拥有录制操作的能力,用户只需要通过Soloπ执行用例步骤,Soloπ就能够将用户的操作记录下来,并且支持在各个设备上进行回放,这一切都能够在手机上独立完成。详见[录制回放](../../wikis/RecordCase)一篇。 +SoloPi JSON 可以转化为其他自动化脚本,目前支持 Appium 和 Macaca ,可以前往 https://github.com/soloPi/SoloPi-Convertor 下载体验,欢迎Watch、Star、Fork 三连。 #### 性能工具 ![性能工具](assets/performance.gif) -**[性能工具使用视频](videos/性能工具.mov)** +**[性能工具使用视频](https://gw.alipayobjects.com/os/basement_prod/1996390b-9ec8-4046-8ce8-459afa05d6c5.mov)** -**[响应耗时计算使用视频](videos/响应耗时计算.mov)** +**[响应耗时计算使用视频](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)一篇。 #### 一机多控 ![一机多控](assets/oneToMany.gif) -**[一机多控使用视频](videos/一机多控.mov)** +**[一机多控使用视频](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) @@ -170,21 +178,22 @@ Soloπ支持通过操作一台主机设备来控制多台从机设备,不需 面向行业测试相关从业人员,对工具有什么意见或者建议的话也欢迎Issue、PR或加群讨论。 -- 钉钉群: +- 钉钉群(一群已满,请添加二群): -![group](assets/group.jpeg) +![group](https://gw.alipayobjects.com/mdn/rms_e29b5f/afts/img/A*JrDwQ4qkVBcAAAAAAAAAAAAAARQnAQ) * 微信群: - 添加好友后回复加群。 - - ![wechatGroup](assets/wechatGroup.jpg) + **目前微信群已满,推荐加入钉钉群** + **除了钉钉群外,我们在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/licenses/COPY.BSD2CLAUSE b/licenses/COPY.BSD2CLAUSE new file mode 100644 index 0000000..1e01141 --- /dev/null +++ b/licenses/COPY.BSD2CLAUSE @@ -0,0 +1,23 @@ +Copyright (c) 2015-2016, Atlassian Pty Ltd +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. + +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. 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 989a30a..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 'org.greenrobot:eventbus:3.1.1' - 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 b7bf0df..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,29 +113,12 @@ void *(**On*Event); # OkHttp platform used only on JVM and when Conscrypt dependency is available. -dontwarn okhttp3.internal.platform.ConscryptPlatform -#eventbus --keepclassmembers class ** { -@org.greenrobot.eventbus.Subscribe ; -} -# 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 { *; } +-keep class com.alipay.hulu.bean.GithubReleaseBean$AuthorBean { *; } +-keep class com.alipay.hulu.bean.GithubReleaseBean$AssetsBean { *; } +-keep class com.alipay.hulu.bean.GithubReleaseBean$AssetsBean$UploaderBean { *; } # 三方库 -keep class com.cgutman.adblib.** {*;} @@ -188,69 +126,17 @@ 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 {*;} - --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 { *; } - --keep enum org.greenrobot.eventbus.ThreadMode { *; } -# Only required if you use AsyncExecutor --keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent { -(java.lang.Throwable); -} - --dontwarn android.support.v4.** --keep class android.support.** {*;} -keepattributes Exceptions,InnerClasses,Signature -#视频直播混淆 + #fastjson -dontwarn com.alibaba.fastjson.** -keep class com.alibaba.fastjson.** { *; } + +# 性能数据上报混淆 +-keep class com.alipay.hulu.util.RecordUtil$RecordUploadData { *; } +-keep class com.alipay.hulu.util.RecordUtil$UploadData { *; } + # fresco -dontwarn javax.annotation.** #保留混淆mapping文件 diff --git a/src/app/src/main/AndroidManifest.xml b/src/app/src/main/AndroidManifest.xml index 2768d59..7728b8b 100644 --- a/src/app/src/main/AndroidManifest.xml +++ b/src/app/src/main/AndroidManifest.xml @@ -14,9 +14,8 @@ ~ limitations under the License. --> + xmlns:tools="http://schemas.android.com/tools" + package="com.alipay.hulu"> @@ -26,6 +25,9 @@ + + + @@ -47,15 +49,19 @@ + android:requestLegacyExternalStorage="true" + android:theme="@style/AppTheme" + tools:replace="android:theme"> @@ -64,83 +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" /> + + + + - - - - - - - - - + + @@ -152,7 +154,7 @@ diff --git a/src/app/src/main/assets/NOTICE.html b/src/app/src/main/assets/NOTICE.html index 98e5558..3cd8495 100644 --- a/src/app/src/main/assets/NOTICE.html +++ b/src/app/src/main/assets/NOTICE.html @@ -2,7 +2,7 @@ -LICENSE -

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

EventBus

https://github.com/greenrobot/EventBus

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

EventBus 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

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 6e5030d..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,20 +16,37 @@ 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.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; +import java.util.HashSet; +import java.util.Set; + /** * Created by lezhou.wyl on 2018/1/28. */ @@ -39,6 +56,8 @@ public abstract class BaseActivity extends AppCompatActivity { private boolean canShowDialog; + private Set fragmentTags = new HashSet<>(); + private static Toast toast; private ProgressDialog progressDialog; @@ -52,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); } // 为了正常初始化 @@ -66,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(); @@ -100,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(); @@ -109,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短时间提示 * @@ -121,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(); } }); @@ -136,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长时间提示 * @@ -148,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() { @@ -184,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); + } } } }); @@ -212,4 +287,48 @@ private void getScreenSizeInfo() { getWindowManager().getDefaultDisplay().getSize(DeviceInfoUtil.curScreenSize); 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 + */ + public void addFragmentTag(String tag) { + fragmentTags.add(tag); + } + + public Set getAllFragmentTags() { + return new HashSet<>(fragmentTags); + } + + /** + * 根据tag查找fragment + * @param tag + * @return + */ + public Fragment getFragmentByTag(String tag) { + FragmentManager supported = getSupportFragmentManager(); + if (supported != null) { + return supported.findFragmentByTag(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 4a90196..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 @@ -16,30 +16,53 @@ package com.alipay.hulu.activity; 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.provider.Settings; +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; +import android.widget.CheckBox; import android.widget.TextView; import com.alipay.hulu.R; +import com.alipay.hulu.adapter.BatchExecutionListAdapter; +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.shared.io.bean.RecordCaseInfo; +import com.alipay.hulu.shared.node.utils.AppUtil; import com.alipay.hulu.ui.HeadControlPanel; -import com.alipay.hulu.common.utils.MiscUtil; +import com.alipay.hulu.util.CaseReplayUtil; +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.Arrays; +import java.util.List; /** * Created by lezhou.wyl on 2018/8/19. */ - -public class BatchExecutionActivity extends BaseActivity { +public class BatchExecutionActivity extends BaseActivity + implements BatchExecutionListAdapter.Delegate , TagFlowLayout.OnTagClickListener{ private ViewPager mPager; + private CheckBox mRestartApp; private TabLayout mTabLayout; private HeadControlPanel mHeadPanel; + private TagFlowLayout tagGroup; + private final List currentCases = new ArrayList<>(); + private TagAdapter tagAdapter; + private Button startExecutionBtn; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -50,15 +73,14 @@ 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) { finish(); } }); - //FIXME 暂时不显示Header - mHeadPanel.setVisibility(View.GONE); + mPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTabLayout)); mTabLayout.setupWithViewPager(mPager); mTabLayout.setTabGravity(TabLayout.GRAVITY_FILL); @@ -71,11 +93,94 @@ public void run() { } }); + // 选择项 + currentCases.clear(); + tagAdapter = new TagAdapter(currentCases) { + @Override + public View getView(FlowLayout parent, int position, RecordCaseInfo o) { + View tag = LayoutInflater.from(BatchExecutionActivity.this).inflate(R.layout.item_batch_execute_tag, parent, false); + TextView title = (TextView) tag.findViewById(R.id.batch_execute_tag_name); + title.setText(o.getCaseName()); + return tag; + } + }; + tagGroup = (TagFlowLayout) findViewById(R.id.batch_execute_tag_group); + tagGroup.setMaxSelectCount(0); + tagGroup.setAdapter(tagAdapter); + tagGroup.setOnTagClickListener(this); + CustomPagerAdapter pagerAdapter = new CustomPagerAdapter(getSupportFragmentManager()); mPager.setAdapter(pagerAdapter); + mRestartApp = (CheckBox) findViewById(R.id.batch_execute_restart); + + startExecutionBtn = (Button) findViewById(R.id.batch_execute_start_btn); + + startExecutionBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (currentCases.size() == 0) { + toastShort(getString(R.string.batch__select_case)); + return; + } + + PermissionUtil.OnPermissionCallback callback = new PermissionUtil.OnPermissionCallback() { + @Override + public void onPermissionResult(boolean result, String reason) { + if (result) { + CaseReplayUtil.startReplayMultiCase(currentCases, mRestartApp.isChecked()); + startApp(currentCases.get(0).getTargetAppPackage()); + } + } + }; + checkPermissions(callback); + } + }); } + @Override + public void onItemAdd(RecordCaseInfo caseInfo) { + currentCases.add(caseInfo); + updateExecutionTag(); + } + + public void updateExecutionTag() { + tagAdapter.notifyDataChanged(); + } + + @Override + public boolean onTagClick(View view, int position, FlowLayout parent) { + currentCases.remove(position); + updateExecutionTag(); + 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 + */ + private void checkPermissions(PermissionUtil.OnPermissionCallback callback) { + // 高权限,悬浮窗权限判断 + PermissionUtil.requestPermissions(Arrays.asList("adb", Settings.ACTION_ACCESSIBILITY_SETTINGS), + this, callback); + } private static class CustomPagerAdapter extends FragmentPagerAdapter { @@ -99,7 +204,4 @@ public int getCount() { return PAGES.length; } } - - - } \ No newline at end of file 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 a38c7fd..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 @@ -16,51 +16,54 @@ 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.v4.content.ContextCompat; -import android.text.TextUtils; -import android.util.Log; +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.widget.AdapterView; -import android.widget.Button; -import android.widget.EditText; -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.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.common.utils.StringUtil; +import com.alipay.hulu.fragment.CaseDescEditFragment; +import com.alipay.hulu.fragment.CaseStepEditFragment; import com.alipay.hulu.shared.io.bean.RecordCaseInfo; import com.alipay.hulu.shared.io.db.GreenDaoManager; import com.alipay.hulu.ui.HeadControlPanel; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + /** * 用例编辑Activity */ -public class CaseEditActivity extends BaseActivity implements AdapterView.OnItemSelectedListener { +public class CaseEditActivity extends BaseActivity { private static final String TAG = "CaseEditActivity"; - public static final String RECORD_CASE_EXTRA = "record_case"; + private RecordCaseInfo mRecordCase; - private HeadControlPanel mHeadPanel; + public static final String RECORD_CASE_EXTRA = "record_case"; - private EditText mCaseName; + private List> caseSaveListeners = new ArrayList<>(); - private EditText mCaseDesc; + private boolean shouldSave = true; - private Button updateButton; + private boolean saved = false; - private RecordCaseInfo mRecordCase; + private HeadControlPanel mHeadPanel; - private String operationMode = null; + private TabLayout tabLayout; + private ViewPager viewPager; - private int caseVersion = 0; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -75,14 +78,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { */ private void initView() { setContentView(R.layout.activity_edit_case); + mHeadPanel = (HeadControlPanel) findViewById(R.id.case_edit_head); - // 获取各项控件 - mHeadPanel = (HeadControlPanel) findViewById(R.id.head_edit_case); - mCaseName = (EditText) findViewById(R.id.case_name); - mCaseDesc = (EditText) findViewById(R.id.case_desc); - - // 获取button - updateButton = (Button) findViewById(R.id.button_update_case); + tabLayout = (TabLayout) findViewById(R.id.case_edit_tab_layout); + viewPager = (ViewPager) findViewById(R.id.case_edit_pager); } /** @@ -92,67 +91,88 @@ private void initData() { int caseId = getIntent().getIntExtra(RECORD_CASE_EXTRA, 0); mRecordCase = CaseStepHolder.getCase(caseId); - mHeadPanel.setMiddleTitle("用例编辑"); - mHeadPanel.setBackIconClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - CaseEditActivity.this.finish(); - } - }); - if (mRecordCase == null) { LogUtil.e(TAG, "There is no record case"); return; } - // 如果有高级设置 - if (!StringUtil.isEmpty(mRecordCase.getAdvanceSettings())) { - AdvanceCaseSetting setting = JSON.parseObject(mRecordCase.getAdvanceSettings(), - AdvanceCaseSetting.class); - caseVersion = setting.getVersion(); - operationMode = setting.getDescriptorMode(); - } + saved = false; + caseSaveListeners.clear(); + mHeadPanel.setMiddleTitle(getString(R.string.activity__case_edit)); + mHeadPanel.setBackIconClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onBackPressed(); + } + }); - mCaseName.setText(mRecordCase.getCaseName()); - mCaseDesc.setText(mRecordCase.getCaseDesc()); - - updateButton.setOnClickListener(new View.OnClickListener() { + mHeadPanel.setInfoIconClickListener(R.drawable.icon_save, new View.OnClickListener() { @Override public void onClick(View v) { - if (TextUtils.isEmpty(mCaseName.getText())) { - toastShort("用例名称不能为空"); - return; - } + updateLocalCase(); + } + }); - wrapRecordCase(); - doUpdateCase(); + 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); } }); + + CustomPagerAdapter pagerAdapter = new CustomPagerAdapter(getSupportFragmentManager(), this); + viewPager.setAdapter(pagerAdapter); + } + + @Override + public void onBackPressed() { + if (shouldSave && !saved) { + 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(); + } + }, getString(R.string.constant__no), new Runnable() { + @Override + public void run() { + finish(); + } + }); + } else { + finish(); + } } /** * 包装用例信息 */ - private void wrapRecordCase() { - mRecordCase.setCaseName(mCaseName.getText().toString()); - mRecordCase.setCaseDesc(mCaseDesc.getText().toString()); - - AdvanceCaseSetting advanceCaseSetting = new AdvanceCaseSetting(); - advanceCaseSetting.setDescriptorMode(operationMode); - advanceCaseSetting.setVersion(caseVersion); - - mRecordCase.setAdvanceSettings(JSON.toJSONString(advanceCaseSetting)); + public void wrapRecordCase() { + for (WeakReference listenerRef: caseSaveListeners) { + if (listenerRef.get() != null) { + listenerRef.get().onCaseSave(); + } + } } - private void doUpdateCase() { + /** + * 更新本地用例 + */ + private void updateLocalCase() { BackgroundExecutor.execute(new Runnable() { @Override public void run() { - mRecordCase.setGmtModify(System.currentTimeMillis()); + wrapRecordCase(); GreenDaoManager.getInstance().getRecordCaseInfoDao().save(mRecordCase); - toastShort("更新成功"); - InjectorService.g().pushMessage(NewRecordActivity.NEED_REFRESH_CASES_LIST); + toastShort(getString(R.string.case__update_success)); + InjectorService.g().pushMessage(NewRecordActivity.NEED_REFRESH_LOCAL_CASES_LIST); + saved = true; } }); } @@ -166,18 +186,50 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } } - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (position == 0) { - ((TextView) view).setTextColor(ContextCompat - .getColor(this, R.color.hint_color)); - } else { - ((TextView) view).setTextColor(Color.BLACK); - } + public RecordCaseInfo getRecordCase() { + return mRecordCase; } - @Override - public void onNothingSelected(AdapterView parent) { + private static class CustomPagerAdapter extends FragmentPagerAdapter { + private RecordCaseInfo caseInfo; + private WeakReference ref; + + public CustomPagerAdapter(FragmentManager fm, CaseEditActivity activity) { + super(fm); + this.caseInfo = activity.mRecordCase; + ref = new WeakReference<>(activity); + } + + @Override + public Fragment getItem(int position) { + if (position == 1) { + CaseDescEditFragment fragment = CaseDescEditFragment.getInstance(caseInfo); + CaseEditActivity activity = ref.get(); + if (activity != null) { + activity.caseSaveListeners.add(new WeakReference(fragment)); + } + return fragment; + } else { + CaseStepEditFragment fragment = CaseStepEditFragment.getInstance(caseInfo); + CaseEditActivity activity = ref.get(); + if (activity != null) { + activity.caseSaveListeners.add(new WeakReference(fragment)); + } + return fragment; + } + } + + @Override + public CharSequence getPageTitle(int position) { + return position == 1? StringUtil.getString(R.string.case_edit__info): StringUtil.getString(R.string.case_edit__steps); + } + @Override + public int getCount() { + return 2; + } + } + public interface OnCaseSaveListener { + void onCaseSave(); } } 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 f7b5eea..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,19 +26,39 @@ 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; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.fragment.ReplayLogFragment; import com.alipay.hulu.fragment.ReplayMainResultFragment; import com.alipay.hulu.fragment.ReplayScreenShotFragment; import com.alipay.hulu.fragment.ReplayStepFragment; import com.alipay.hulu.ui.HeadControlPanel; +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"; @@ -75,18 +89,25 @@ 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() { - mHeadPanel.setMiddleTitle("回放结果"); + Intent intent = getIntent(); + int id = intent.getIntExtra("data", 0); + result = CaseStepHolder.getResult(id); + if (result == null) { + return; + } + + mHeadPanel.setMiddleTitle(getString(R.string.activity__replay_result)); mHeadPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -94,35 +115,60 @@ public void onClick(View v) { } }); + // 如果没保存过 + File f = new File(FileUtils.getSubDir("replay"), result.getCaseName() + "_" + result.getEndTime().getTime()); + if (!f.exists()) { + // 保存结果 + mHeadPanel.setInfoIconClickListener(R.drawable.icon_save, new View.OnClickListener() { + @Override + public void onClick(View v) { + showProgressDialog(getString(R.string.case_replay__saving)); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + File result = saveReplayResult(); + dismissProgressDialog(); + if (result != null) { + LauncherApplication.getInstance().showDialog( + CaseReplayResultActivity.this, + getString(R.string.replay__save_result_to, result.getPath()), + getString(R.string.constant__sure), null); + } else { + toastLong(getString(R.string.replay__save_failed)); + } + } + }); + } + }); + } + 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() { - setIndicator(mTabLayout, 0, 0); - } - }); - - - Intent intent = getIntent(); - int id = intent.getIntExtra("data", 0); - result = CaseStepHolder.getResult(id); - if (result == null) { - return; - } +// 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); @@ -132,6 +178,99 @@ public void run() { mPager.setAdapter(mAdapter); } + /** + * 保存回放结果 + * @return + */ + private File saveReplayResult() { + // 生成根目录 + File root = new File(FileUtils.getSubDir("replay"), result.getCaseName() + "_" + result.getEndTime().getTime()); + boolean mkResult = root.mkdirs(); + if (!root.exists() || !root.isDirectory()) { + return null; + } + + File info = new File(root, "info.json"); + 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) { + LogUtil.e(TAG, "输出结果失败", e); + } + + File logFile = new File(root, "running.log"); + try { + FileUtils.copyFile(new File(result.getLogFile()), logFile); + } catch (IOException e) { + LogUtil.e(TAG, "输出日志失败", e); + } + + File stepsFile = new File(root, "steps.json"); + try { + JSON.writeJSONStringTo(result.getCurrentOperationLog(), new FileWriter(stepsFile)); + } catch (IOException e) { + 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) { + LogUtil.e(TAG, "输出步骤信息失败", e); + } + return root; + } + private void setIndicator(TabLayout tabs, int leftDip, int rightDip) { Class tabLayout = tabs.getClass(); Field tabStrip; @@ -194,15 +333,17 @@ public int getCount() { public CharSequence getPageTitle(int position) { switch (position) { case 0: - return "回放结果"; + return StringUtil.getString(R.string.replay__replay_result); case 1: - return "用例步骤"; + return StringUtil.getString(R.string.replay__case_steps); case 2: - return "运行日志"; + return StringUtil.getString(R.string.replay__running_log); case 3: - return "用例截图"; + return StringUtil.getString(R.string.replay__case_screenshot); } return ""; } } + + } diff --git a/src/app/src/main/java/com/alipay/hulu/activity/CaseStepEditActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/CaseStepEditActivity.java deleted file mode 100644 index c187653..0000000 --- a/src/app/src/main/java/com/alipay/hulu/activity/CaseStepEditActivity.java +++ /dev/null @@ -1,648 +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.activity; - -import android.app.Dialog; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -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.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.View; -import android.view.WindowManager; -import android.view.animation.AnimationUtils; -import android.view.animation.LayoutAnimationController; -import android.widget.AdapterView; -import android.widget.ImageView; -import android.widget.TextView; - -import com.alibaba.fastjson.JSON; -import com.alipay.hulu.R; -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; -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.utils.ContextUtil; -import com.alipay.hulu.common.utils.LogUtil; -import com.alipay.hulu.common.utils.StringUtil; -import com.alipay.hulu.event.ScanSuccessEvent; -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.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.export.bean.OperationStep; -import com.alipay.hulu.shared.node.utils.LogicUtil; -import com.alipay.hulu.ui.HeadControlPanel; -import com.alipay.hulu.ui.MaxHeightScrollView; -import com.alipay.hulu.ui.TwoLevelSelectLayout; -import com.alipay.hulu.util.FunctionSelectUtil; -import com.yydcdut.sdlv.Menu; -import com.yydcdut.sdlv.MenuItem; -import com.yydcdut.sdlv.SlideAndDragListView; -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.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import static com.alipay.hulu.shared.node.utils.LogicUtil.SCOPE; - -/** - * Created by qiaoruikai on 2019/2/18 8:05 PM. - */ -public class CaseStepEditActivity extends BaseActivity implements TagFlowLayout.OnTagClickListener { - private static final String TAG = "CaseStepEditActivity"; - private boolean isOverrideInstall = false; - - private RecordCaseInfo recordCase; - - private HeadControlPanel head; - - private TagFlowLayout tagGroup; - - private SlideAndDragListView dragList; - - private List stepList; - - private List dragEntities; - - private AtomicInteger currentIdx; - - private CaseStepAdapter adapter; - - private boolean saved = false; - - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_case_step_edit); - - initView(); - initData(); - } - - @Subscriber(value = @Param(sticky = false), thread = RunningThread.MAIN_THREAD) - public void onScanEvent(final ScanSuccessEvent event) { - switch (event.getType()) { - case ScanSuccessEvent.SCAN_TYPE_SCHEME: - // 向handler发送请求 - OperationMethod method = new OperationMethod(PerformActionEnum.JUMP_TO_PAGE); - method.putParam(OperationExecutor.SCHEME_KEY, event.getContent()); - OperationStep step = new OperationStep(); - step.setOperationMethod(method); - step.setOperationIndex(currentIdx.get()); - step.setOperationId(stepList.get(stepList.size() - 1).getOperationId()); - - CaseStepAdapter.MyDataWrapper wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); - - dragEntities.add(wrapper); - - adapter.notifyDataSetChanged(); - - // 录制模式需要记录下 - - break; - default: - break; - } - } - - /** - * 初始化界面 - */ - private void initView() { - head = (HeadControlPanel) findViewById(R.id.case_step_edit_head); - - // 加载相关控件 - tagGroup = (TagFlowLayout) findViewById(R.id.case_step_edit_tag_group); - dragList = (SlideAndDragListView) findViewById(R.id.case_step_edit_drag_list); - - LayoutAnimationController controller = new LayoutAnimationController( - AnimationUtils.loadAnimation(this, android.R.anim.fade_in)); - controller.setDelay(0); - dragList.setLayoutAnimation(controller); - } - - /** - * 初始化用例数据 - */ - private void initData() { - int id = getIntent().getIntExtra("case", 0); - recordCase = CaseStepHolder.getCase(id); - if (recordCase == null) { - return; - } - - head.setMiddleTitle(recordCase.getCaseName()); - - currentIdx = new AtomicInteger(); - - head.setBackIconClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } - }); - - head.setInfoIconClickListener(R.drawable.icon_save, new View.OnClickListener() { - @Override - public void onClick(View v) { - saveRecord(); - saved = true; - } - }); - saved = false; - - GeneralOperationLogBean generalOperation = JSON.parseObject(recordCase.getOperationLog(), GeneralOperationLogBean.class); - if (generalOperation.getSteps() != null) { - stepList = generalOperation.getSteps(); - } - - // 可以全新添加步骤 - if (stepList == null) { - stepList = new ArrayList<>(); - } - - dragEntities = new ArrayList<>(stepList.size() + 1); - final List stepTags = new ArrayList<>(stepList.size() + 2); - - // 每一步添加一个实体 - stepTags.add("新步骤"); - for (OperationStep step: stepList) { - CaseStepAdapter.MyDataWrapper entity = new CaseStepAdapter.MyDataWrapper(clone(step), currentIdx.getAndIncrement()); - dragEntities.add(entity); - stepTags.add(step.getOperationMethod().getActionEnum().getDesc()); - - String drag = step.getOperationMethod().getParam(SCOPE); - if (drag != null) { - entity.scopeTo = Integer.parseInt(drag) + entity.idx; - } - } - - tagGroup.setMaxSelectCount(0); - tagGroup.setAdapter(new TagAdapter(stepTags) { - @Override - public View getView(FlowLayout parent, int position, String o) { - View tag = LayoutInflater.from(CaseStepEditActivity.this).inflate(R.layout.item_case_step_tag, null); - TextView title = (TextView) tag.findViewById(R.id.case_step_edit_tag_title); - ImageView icon = (ImageView) tag.findViewById(R.id.case_step_edit_tag_icon); - - if (position == 0) { - title.setText("新步骤"); - icon.setImageResource(R.drawable.case_step_add); - } else { - - // 加载下 - OperationStep step = stepList.get(position - 1); - PerformActionEnum actionEnum = step.getOperationMethod().getActionEnum(); - - // 设置资源 - title.setText(actionEnum.getDesc()); - icon.setImageResource(actionEnum.getIcon()); - } - return tag; - } - }); - tagGroup.setOnTagClickListener(this); - - // 用例adapter - adapter = new CaseStepAdapter(this, dragEntities); - - // 设置菜单相关样式 - int dp64 = ContextUtil.dip2px(this, 64); - int colorWhile; - int colorIf; - if (Build.VERSION.SDK_INT >= 23) { - colorWhile = getColor(R.color.colorStatusBlue); - colorIf = getColor(R.color.colorStatusRed); - } else { - colorWhile = getResources().getColor(R.color.colorStatusBlue); - colorIf = getResources().getColor(R.color.colorStatusRed); - } - - // 转换模式 - Menu menu = new Menu(true, 0); - menu.addItem(new MenuItem.Builder().setText("转换为IF").setTextColor(Color.WHITE).setWidth(dp64) - .setDirection(MenuItem.DIRECTION_RIGHT) - .setBackground(new ColorDrawable(colorIf)).build()); - menu.addItem(new MenuItem.Builder().setText("转换为WHILE").setTextColor(Color.WHITE) - .setWidth(dp64) - .setDirection(MenuItem.DIRECTION_RIGHT) - .setBackground(new ColorDrawable(colorWhile)).build()); - - // 空项 - Menu controlMenu = new Menu(false, 1); - - dragList.setMenu(menu, controlMenu); - dragList.setDividerHeight(0); - dragList.setAdapter(adapter); - dragList.setOnMenuItemClickListener(new SlideAndDragListView.OnMenuItemClickListener() { - @Override - public int onMenuItemClick(View v, int itemPosition, int buttonPosition, int direction) { - if (direction == MenuItem.DIRECTION_RIGHT) { - // 全部操作均支持转化 - 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) { - method.setActionEnum(PerformActionEnum.WHILE); - wrapper.scopeTo = wrapper.idx + 1; - } - // 设置assert条件 - method.putParam(LogicUtil.CHECK_PARAM, LogicUtil.ASSERT_ACTION_PREFIX + origin.getCode()); - adapter.notifyDataSetChanged(); - - return Menu.ITEM_SCROLL_BACK; - } - return 0; - } - }); - - dragList.setOnDragDropListener(adapter); - dragList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - showEditDialog(dragEntities.get(position)); - } - }); - } - - @Override - public boolean onTagClick(View view, int position, FlowLayout parent) { - if (position == 0) { - showAddFunctionView(); - return true; - } - - OperationStep step = stepList.get(position - 1); - CaseStepAdapter.MyDataWrapper entity = new CaseStepAdapter.MyDataWrapper(clone(step), currentIdx.getAndIncrement()); - - // 如果是if和while,需要设置为0 - OperationMethod method = step.getOperationMethod(); - if (method.getActionEnum() == PerformActionEnum.IF || method.getActionEnum() == PerformActionEnum.WHILE) { - entity.scopeTo = 0; - } - - dragEntities.add(entity); - // 添加用例步骤 - adapter.notifyDataSetChanged(); - - return true; - } - - /** - * 将scopeTo信息存储到param中 - */ - private void saveScopeInfo() { - for (int i = 0; i < dragEntities.size(); i++) { - CaseStepAdapter.MyDataWrapper wrapper = dragEntities.get(i); - - if (wrapper.scopeTo > -1) { - for (int j = i + 1; j < dragEntities.size(); j++) { - CaseStepAdapter.MyDataWrapper to = dragEntities.get(j); - - if (to.idx == wrapper.scopeTo) { - wrapper.currentStep.getOperationMethod().putParam(SCOPE, Integer.toString(j - i)); - break; - } - } - } - } - } - - /** - * 保存用例 - */ - private void saveRecord() { - // 同步下scope信息 - saveScopeInfo(); - - List operations = new ArrayList<>(dragEntities.size() + 1); - for (CaseStepAdapter.MyDataWrapper wrapper: dragEntities) { - operations.add(wrapper.currentStep); - } - - GeneralOperationLogBean logBean = new GeneralOperationLogBean(); - logBean.setSteps(operations); - - recordCase.setOperationLog(JSON.toJSONString(logBean)); - doUpdateCase(recordCase); - } - - /** - * 显示编辑框 - * @param wrapper - */ - private void showEditDialog(final CaseStepAdapter.MyDataWrapper wrapper) { - final View v = LayoutInflater.from(this).inflate(R.layout.dialog_case_step_edit, null); - final RecyclerView r = (RecyclerView) v.findViewById(R.id.dialog_case_step_edit_recycler); - MaxHeightScrollView scroll = (MaxHeightScrollView) v.findViewById(R.id.dialog_case_step_edit_scroll); - DisplayMetrics dm = new DisplayMetrics(); - ((WindowManager) LauncherApplication.getInstance().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(dm); - final int height = dm.heightPixels; - scroll.setMaxHeight(height / 2); - - // 拷贝一份 - final OperationStep clone = clone(wrapper.currentStep); - r.setLayoutManager(new LinearLayoutManager(this)); - - final CaseStepNodeAdapter nodeAdapter; - 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"); - tab.addTab(tabItem); - tabItem.select(); - nodeAdapter = new CaseStepNodeAdapter(clone.getOperationNode()); - } else { - nodeAdapter = null; - } - - TabLayout.Tab tabItem = tab.newTab(); - tabItem.setText("Method"); - tab.addTab(tabItem); - - // 配置后续列表 - final List laterList; - int curPos = dragEntities.indexOf(wrapper); - if (wrapper.scopeTo > -1) { - laterList = dragEntities.subList(curPos + 1, dragEntities.size()); - - boolean flag = false; - for (int pos = curPos + 1; pos < dragEntities.size(); pos++) { - if (dragEntities.get(pos).idx == wrapper.scopeTo) { - clone.getOperationMethod().putParam(SCOPE, Integer.toString(pos - curPos)); - flag = true; - break; - } - } - if (!flag) { - clone.getOperationMethod().putParam(SCOPE, "1"); - } - } else { - laterList = new ArrayList<>(); - } - - 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")) { - r.setAdapter(nodeAdapter); - } else { - r.setAdapter(paramAdapter); - } - } - - @Override - public void onTabUnselected(TabLayout.Tab tab) { - - } - - @Override - public void onTabReselected(TabLayout.Tab tab) { - - } - }); - DialogInterface.OnClickListener dialogClick = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == Dialog.BUTTON_POSITIVE) { - wrapper.currentStep = clone; - - // 如果需要配置scopeTo - if (wrapper.scopeTo > -1) { - int pos = Integer.parseInt(clone.getOperationMethod().getParam(SCOPE)) - 1; - wrapper.scopeTo = laterList.get(pos).idx; - } - - adapter.notifyDataSetChanged(); - } - } - }; - - final AlertDialog dialog = new AlertDialog.Builder(this) - .setView(v).setPositiveButton("确定", dialogClick) - .setNegativeButton("取消", dialogClick) - .setTitle(clone.getOperationMethod().getActionEnum().getDesc()).create(); - dialog.show(); - - // 选择第一个 - dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); - } - - /** - * 基于Parcel拷贝一份数据 - * @param origin - * @return - */ - private OperationStep clone(OperationStep origin) { - Parcel p = Parcel.obtain(); - origin.writeToParcel(p, 0); - p.setDataPosition(0); - return OperationStep.CREATOR.createFromParcel(p); - } - - /** - * 更新用例 - * @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_CASES_LIST); - } - }); - } - - @Override - public void onBackPressed() { - // 如果没有点过保存,问一下是否需要保存用例 - if (!saved) { - LauncherApplication.getInstance().showDialog(this, "是否保存用例", "是", new Runnable() { - @Override - public void run() { - saveRecord(); - finish(); - } - }, "否", new Runnable() { - @Override - public void run() { - finish(); - } - }); - } else { - finish(); - } - } - - /** - * 显示添加操作界面 - */ - private void showAddFunctionView() { - FunctionSelectUtil.showFunctionView(this, 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) { - - if (StringUtil.equals(method.getParam("scan"), "1")) { - // 注册下Service - InjectorService injectorService = LauncherApplication.getInstance().findServiceByName(InjectorService.class.getName()); - injectorService.register(CaseStepEditActivity.this); - - - Intent intent = new Intent(CaseStepEditActivity.this, QRScanActivity.class); - - intent.putExtra(QRScanActivity.KEY_SCAN_TYPE, ScanSuccessEvent.SCAN_TYPE_SCHEME); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - return; - } - } - - 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); - } - - CaseStepAdapter.MyDataWrapper wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); - - // if和while设置下scope - if (method.getActionEnum() == PerformActionEnum.IF || method.getActionEnum() == PerformActionEnum.WHILE) { - wrapper.scopeTo = wrapper.idx; - } - - dragEntities.add(wrapper); - - adapter.notifyDataSetChanged(); - } - - @Override - public void onCancel() { - - } - }); - } - - 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<>(); - - // 初始化二级菜单 - static { - // 全局操作 - GLOBAL_KEYS.add("device"); - GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_device_operation); - List gDeviceActions = new ArrayList<>(); - gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.BACK)); - gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.HOME)); - gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.HANDLE_ALERT)); - gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCREENSHOT)); - gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.SLEEP)); - gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.EXECUTE_SHELL)); - gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.NOTIFICATION)); - gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.RECENT_TASK)); - GLOBAL_ACTION_MAP.put("device", gDeviceActions); - - GLOBAL_KEYS.add("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.KILL_PROCESS)); - GLOBAL_ACTION_MAP.put("app", gAppActions); - - GLOBAL_KEYS.add("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("logic"); - GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_logic); - List gLoopActions = new ArrayList<>(); - gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.IF)); - gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.WHILE)); - gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.CONTINUE)); - gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.BREAK)); - GLOBAL_ACTION_MAP.put("logic", gLoopActions); - } - - /** - * 转换为菜单 - * @param actionEnum - * @return - */ - private 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/activity/IndexActivity.java b/src/app/src/main/java/com/alipay/hulu/activity/IndexActivity.java index 2bbade3..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,11 +23,13 @@ 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; +import android.webkit.WebSettings; +import android.webkit.WebView; import android.widget.BaseAdapter; import android.widget.GridView; import android.widget.ImageView; @@ -36,25 +39,35 @@ import com.alibaba.fastjson.JSONObject; import com.alipay.hulu.R; import com.alipay.hulu.activity.entry.EntryActivity; +import com.alipay.hulu.bean.GithubReleaseBean; import com.alipay.hulu.common.application.LauncherApplication; 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.DeviceInfoUtil; +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; import com.alipay.hulu.util.SystemUtil; +import com.alipay.hulu.util.UpgradeUtil; import com.alipay.hulu.util.ZipUtil; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; + 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; @@ -72,7 +85,6 @@ public class IndexActivity extends BaseActivity { private static final String TAG = IndexActivity.class.getSimpleName(); - private static final String DISPLAY_ALERT_INFO = "displayAlertInfo"; private HeadControlPanel mPanel; private GridView mGridView; @@ -86,24 +98,80 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { initData(); loadOthers(); - // 免责弹窗 - boolean showDisplay = SPService.getBoolean(DISPLAY_ALERT_INFO, true); - if (showDisplay) { - new AlertDialog.Builder(this).setTitle("免责声明") - .setMessage(R.string.disclaimer) - .setPositiveButton("确定", new DialogInterface.OnClickListener() { +// SPService.putBoolean(SPService.KEY_USE_EASY_MODE, true); + + // check update + 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() { + UpgradeUtil.checkForUpdate(new UpgradeUtil.CheckUpdateListener() { @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); + public void onNoUpdate() { + } - }).setNegativeButton("不再提示", new DialogInterface.OnClickListener() { + @Override - public void onClick(DialogInterface dialog, int which) { - SPService.putBoolean(DISPLAY_ALERT_INFO, false); - dialog.dismiss(); + public void onNewUpdate(final GithubReleaseBean release) { + Parser parser = Parser.builder().build(); + Node document = parser.parse(release.getBody()); + + // text size 16dp + int px = ContextUtil.dip2px(IndexActivity.this, 16); + HtmlRenderer renderer = HtmlRenderer.builder().build(); + String css = "
"; + final String content = css + renderer.render(document) + ""; + + runOnUiThread(new Runnable() { + @Override + public void run() { + WebView webView = new WebView(IndexActivity.this); + WebSettings webSettings = webView.getSettings(); + webSettings.setUseWideViewPort(true); + webSettings.setLoadWithOverviewMode(true); + webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS); + webView.loadData(content, null, null); + new AlertDialog.Builder(IndexActivity.this).setTitle(getString(R.string.index__new_version, release.getTag_name())) + .setView(webView) + .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(R.string.constant__cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }).show(); + } + }); } - }).show(); + + @Override + public void onUpdateFailed(Throwable t) { + + } + }); + } + }); } + + // 进入主页的trigger + LauncherApplication.getInstance().triggerAtTime(Trigger.TRIGGER_TIME_HOME_PAGE); } /** @@ -150,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) { @@ -160,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(); } }); } @@ -198,12 +282,12 @@ public boolean accept(File pathname) { // 只上传一条,根据修改时间查看 LauncherApplication.getInstance().showDialog( IndexActivity.this, - getString(R.string.index__find_error_log), getString(R.string.constant__yes), new Runnable() { + getString(R.string.index__find_error_log), getString(R.string.constant__sure), new Runnable() { @Override public void run() { reportError(time, errorLog); } - }, "取消", null); + }, getString(R.string.constant__cancel), null); break; } } @@ -275,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); @@ -284,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; @@ -298,8 +402,45 @@ public static class Entry { private Class targetActivity; public Entry(EntryActivity activity, Class target) { - this.iconId = activity.icon(); - this.name = activity.name(); + 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) { + 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(); level = activity.level(); targetActivity = target; @@ -406,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() { @@ -417,27 +560,19 @@ public void onClick(View v) { public void onPermissionResult(boolean result, String reason) { LogUtil.d(TAG, "权限申请耗时:%dms", System.currentTimeMillis() - startTime); if (result) { - if (mPanel != null) { - - // 记录下进入次数 - Integer count = entryCount.getInteger(item.name); - if (count == null) { - count = 1; - } else { - count ++; - } - entryCount.put(item.name, count); - versionsCount.put(Integer.toString(currentVersionCode), entryCount); - SPService.putString(SPService.KEY_INDEX_RECORD, JSON.toJSONString(versionsCount)); - - mPanel.post(new Runnable() { - @Override - public void run() { - Intent intent = new Intent(IndexActivity.this, item.targetActivity); - startActivity(intent); - } - }); + // 记录下进入次数 + Integer count = entryCount.getInteger(item.name); + if (count == null) { + count = 1; + } else { + count ++; } + entryCount.put(item.name, count); + versionsCount.put(Integer.toString(currentVersionCode), entryCount); + SPService.putString(SPService.KEY_INDEX_RECORD, JSON.toJSONString(versionsCount)); + + Intent intent = new Intent(IndexActivity.this, item.targetActivity); + startActivity(intent); } } }); 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 5d3ec7d..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,14 +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.os.Handler; 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,14 +43,13 @@ import com.alipay.hulu.common.utils.HuluCrashHandler; import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.StringUtil; -import com.alipay.hulu.event.AppForegroundEvent; 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 org.greenrobot.eventbus.EventBus; - import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -64,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"; @@ -168,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); @@ -185,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)); } } } @@ -253,7 +256,6 @@ public void onFinish(List resultBeans, Context context) { @Override public void init() { - sInstance = this; // 注册自身信息 @@ -283,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(); } @@ -300,6 +320,21 @@ public List loadAppList() { return packageList; } + /** + * 获取应用列表 + * @return + */ + public void reloadAppList() { + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + synchronized (MyApplication.class) { + loadApplicationList(); + } + } + }); + } + private void registerLifecycleCallbacks() { registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override @@ -310,9 +345,6 @@ public void onActivityCreated(Activity activity, Bundle savedInstanceState) { @Override public void onActivityStarted(Activity activity) { activityCount++; - if (activityCount == 1) { - EventBus.getDefault().post(new AppForegroundEvent(true)); - } } @Override @@ -328,9 +360,6 @@ public void onActivityPaused(Activity activity) { @Override public void onActivityStopped(Activity activity) { activityCount--; - if (activityCount == 0) { - EventBus.getDefault().post(new AppForegroundEvent(false)); - } } @Override @@ -343,6 +372,11 @@ public void onActivityDestroyed(Activity activity) { } }); + + // Init the FileDownloader with the OkHttp3Connection.Creator. + FileDownloader.setupOnApplicationOnCreate(this) + .connectionCreator(new OkHttp3Connection.Creator()) + .commit(); } /** @@ -355,13 +389,16 @@ private void loadApplicationList() { List removedItems = new ArrayList<>(); + String selfPackage = getPackageName(); + boolean displaySystemApp = SPService.getBoolean(SPService.KEY_DISPLAY_SYSTEM_APP, false); + for (ApplicationInfo pack: listPack) { - if ((pack.flags & ApplicationInfo.FLAG_SYSTEM) > 0) { + if (!displaySystemApp && (pack.flags & ApplicationInfo.FLAG_SYSTEM) > 0) { removedItems.add(pack); } // 移除自身 - if (StringUtil.equals(getPackageName(), pack.packageName)) { + if (StringUtil.equals(selfPackage, pack.packageName)) { removedItems.add(pack); } } @@ -415,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); @@ -463,7 +500,7 @@ public Map getAppAndAppName() { this.appName = appName[0]; } else { this.appPackage = "-"; - this.appName = "全局"; + this.appName = getString(R.string.constant_global); } } @@ -475,7 +512,7 @@ public Map getAppAndAppName() { private void initLibraries() { initGreenDao(); - initFileDownloader(); +// initFileDownloader(); curSysInputMethod = Settings.Secure.getString(getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); @@ -510,6 +547,10 @@ private void initGreenDao() { GreenDaoManager.getInstance(); } + public void updateDefaultIme(String ime) { + curSysInputMethod = ime; + } + public static MyApplication getInstance() { return sInstance; } 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 eb83533..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 @@ -18,11 +18,10 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; -import android.os.Build; 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; @@ -33,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; @@ -46,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; @@ -54,36 +51,34 @@ 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.RecordCaseChangedEvent; 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 org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - import java.util.Arrays; import java.util.List; /** * Created by lezhou.wyl on 2018/2/1. */ -@EntryActivity(icon = R.drawable.icon_luxiang, name = "录制回放", permissions = {"adb", "float", "toast:请将Soloπ添加到后台白名单中"}, index = 1, cornerText = "图像", 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(); public static final String NEED_REFRESH_PAGE = "NEED_REFRESH_PAGE"; - public static final String NEED_REFRESH_CASES_LIST = "NEED_REFRESH_CASES_LIST"; + public static final String NEED_REFRESH_LOCAL_CASES_LIST = "NEED_REFRESH_LOCAL_CASES_LIST"; private DrawerLayout mDrawerLayout; @@ -116,7 +111,7 @@ public void setApp(String app) { this.app = app; } - @Subscriber(@Param(value = NEED_REFRESH_CASES_LIST, sticky = false)) + @Subscriber(@Param(value = NEED_REFRESH_LOCAL_CASES_LIST, sticky = false)) public void notifyCaseListChange() { if (!isDestroyed()) { getRecentCaseList(); @@ -126,11 +121,9 @@ public void notifyCaseListChange() { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - InjectorService injectorService = LauncherApplication.getInstance().findServiceByName(InjectorService.class.getName()); - injectorService.register(this); - - EventBus.getDefault().register(this); setContentView(R.layout.activity_record_new); + InjectorService.g().register(this); + initDrawerLayout(); initAppList(); initHeadPanel(); @@ -158,23 +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; - } - - // 启动编辑页 - 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); @@ -189,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("正在加载中"); + } - 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, "环境准备失败", 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("录制回放"); + mPanel.setMiddleTitle(getString(R.string.activity__record)); mPanel.setBackIconClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -350,7 +359,7 @@ private void initAppHeadView() { @Override public void onClick(View v) { if (StringUtil.isEmpty(mCaseName.getText().toString().trim())) { - Toast.makeText(NewRecordActivity.this, "用例名不能为空", Toast.LENGTH_SHORT).show(); + toastShort(R.string.record__case_name_empty); return; } @@ -383,8 +392,7 @@ public void onClick(View v) { @Override public void onPermissionResult(boolean result, String reason) { if (result) { - - showProgressDialog("正在加载中"); + showProgressDialog(getString(R.string.record__preparing)); BackgroundExecutor.execute(new Runnable() { @Override @@ -401,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()); } @@ -410,7 +421,7 @@ public void run() { @Override public void run() { dismissProgressDialog(); - Toast.makeText(NewRecordActivity.this, "环境准备失败", Toast.LENGTH_SHORT).show(); + toastShort(R.string.record__prepare_failed); } }); } @@ -527,15 +538,7 @@ public void onBackPressed() { @Override protected void onDestroy() { super.onDestroy(); - EventBus.getDefault().unregister(this); - } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onRecordCaseChanged(RecordCaseChangedEvent event) { - if (event.getType() == RecordCaseChangedEvent.TYPE_CASE_ADD - || event.getType() == RecordCaseChangedEvent.TYPE_LOCAL_DELETE) { - getRecentCaseList(); - } + InjectorService.g().unregister(this); } @Override 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 3cfab3c..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 @@ -17,19 +17,23 @@ 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 android.view.LayoutInflater; import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import com.alipay.hulu.R; 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. @@ -40,7 +44,6 @@ public class NewReplayListActivity extends BaseActivity { private ViewPager mPager; private TabLayout mTabLayout; private HeadControlPanel mHeadPanel; - private TextView rightTitle; @Override @@ -49,19 +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); - rightTitle = (TextView) mHeadPanel.findViewById(R.id.right_title); - rightTitle.setText(R.string.constant__batch_replay); + mPager = findViewById(R.id.pager); + mTabLayout = findViewById(R.id.tab_layout); + mHeadPanel = findViewById(R.id.head_replay_list); + + // 配置菜单信息 + 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) { @@ -82,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 916f764..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(); @@ -182,8 +222,8 @@ public void notifyScanSuccess(String content) { private void requestCameraPermission() { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { - Snackbar.make(mainLayout, "Soloπ需要权限来展示相机预览", - Snackbar.LENGTH_INDEFINITE).setAction("好的", new View.OnClickListener() { + Snackbar.make(mainLayout, R.string.qr__camera_permission, + Snackbar.LENGTH_INDEFINITE).setAction(R.string.constant__yes, new View.OnClickListener() { @Override public void onClick(View view) { ActivityCompat.requestPermissions(QRScanActivity.this, new String[]{ @@ -192,7 +232,7 @@ public void onClick(View view) { } }).show(); } else { - Snackbar.make(mainLayout, "未获得权限,正在申请相机权限", + Snackbar.make(mainLayout, R.string.qr__requst_permission, Snackbar.LENGTH_SHORT).show(); ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.CAMERA @@ -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 71ce2da..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; @@ -42,15 +39,22 @@ import com.alipay.hulu.common.utils.LogUtil; import com.alipay.hulu.common.utils.PatchProcessUtil; import com.alipay.hulu.common.utils.StringUtil; +import com.alipay.hulu.common.utils.activity.FileChooseDialogActivity; import com.alipay.hulu.common.utils.patch.PatchLoadResult; 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.db.RecordCaseInfoDao; +import com.alipay.hulu.shared.io.util.OperationStepUtil; import com.alipay.hulu.shared.node.action.OperationMethod; 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; @@ -63,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. */ @@ -70,6 +76,8 @@ public class SettingsActivity extends BaseActivity { private static final String TAG = "SettingsActivity"; + private static final int REQUEST_FILE_CHOOSE = 1101; + private HeadControlPanel mPanel; private View mRecordUploadWrapper; @@ -81,12 +89,56 @@ 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; + + private View mBaseDirSettingWrapper; + private TextView mBaseDirSettingInfo; + + private View mOutputCharsetSettingWrapper; + private TextView mOutputCharsetSettingInfo; + private View mAesSeedSettingWrapper; private TextView mAesSeedSettingInfo; @@ -96,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; @@ -126,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) { @@ -149,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) { @@ -169,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) { @@ -181,7 +294,7 @@ public void onDialogPositive(List data) { mPatchListInfo.setText(path); // 更新patch列表 - PatchRequest.updatePatchList(); + PatchRequest.updatePatchList(null); } } } @@ -191,10 +304,246 @@ public void onDialogPositive(List data) { } }); + mOutputCharsetSettingWrapper.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 charset = data.get(0); + SPService.putString(SPService.KEY_OUTPUT_CHARSET, charset); + mOutputCharsetSettingInfo.setText(charset); + } + } + }, getString(R.string.settings__output_charset), + Collections.singletonList(new Pair<>(getString(R.string.settings__output_charset), + SPService.getString(SPService.KEY_OUTPUT_CHARSET)))); + } + }); + + + 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) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.setting__display_system_app) + .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_DISPLAY_SYSTEM_APP, true); + mDisplaySystemAppSettingInfo.setText(R.string.constant__yes); + MyApplication.getInstance().reloadAppList(); + dialog.dismiss(); + } + }).setNegativeButton(R.string.constant__no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_DISPLAY_SYSTEM_APP, false); + mDisplaySystemAppSettingInfo.setText(R.string.constant__no); + MyApplication.getInstance().reloadAppList(); + dialog.dismiss(); + } + }).show(); + } + }); + + 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, getString(R.string.settings__base_dir), "solopi", + FileUtils.getSolopiDir()); + } + }); + 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) { @@ -216,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) { @@ -231,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); } } } @@ -243,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) { @@ -256,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); } }); @@ -264,25 +613,68 @@ public void onDialogPositive(List data) { @Override public void onClick(View v) { new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) - .setMessage("回放时是否高亮待操作控件?") - .setPositiveButton("是", new DialogInterface.OnClickListener() { + .setMessage(R.string.settings__highlight_node) + .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { SPService.putBoolean(SPService.KEY_HIGHLIGHT_REPLAY_NODE, true); - mHightlightSettingInfo.setText("是"); + mHightlightSettingInfo.setText(R.string.constant__yes); dialog.dismiss(); } - }).setNegativeButton("否", new DialogInterface.OnClickListener() { + }).setNegativeButton(R.string.constant__no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { SPService.putBoolean(SPService.KEY_HIGHLIGHT_REPLAY_NODE, false); - mHightlightSettingInfo.setText("否"); + mHightlightSettingInfo.setText(R.string.constant__no); + dialog.dismiss(); + } + }).show(); + } + }); + + // check update + mCheckUpdateSettingWrapper.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + new AlertDialog.Builder(SettingsActivity.this, R.style.SimpleDialogTheme) + .setMessage(R.string.settings__check_update) + .setPositiveButton(R.string.constant__yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(SPService.KEY_CHECK_UPDATE, true); + mCheckUpdateSettingInfo.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_CHECK_UPDATE, false); + mCheckUpdateSettingInfo.setText(R.string.constant__no); dialog.dismiss(); } }).show(); } }); + // 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) { @@ -335,6 +727,10 @@ public void run() { // 加载实例 RecordCaseInfo caseInfo = JSON.parseObject(sb.toString(), RecordCaseInfo.class); + String operationLog = caseInfo.getOperationLog(); + GeneralOperationLogBean log = JSON.parseObject(operationLog, GeneralOperationLogBean.class); + OperationStepUtil.beforeStore(log); + caseInfo.setOperationLog(JSON.toJSONString(log)); GreenDaoManager.getInstance().getRecordCaseInfoDao().insert(caseInfo); @@ -406,80 +802,15 @@ 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("确定", 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("取消", 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); String path = SPService.getString(SPService.KEY_RECORD_SCREEN_UPLOAD); if (StringUtil.isEmpty(path)) { - mRecordScreenUploadInfo.setText("未设置"); + mRecordScreenUploadInfo.setText(R.string.settings__unset); } else { mRecordScreenUploadInfo.setText(path); } @@ -488,7 +819,7 @@ private void initView() { mRecordUploadInfo = (TextView) findViewById(R.id.performance_upload_setting_info); path = SPService.getString(SPService.KEY_PERFORMANCE_UPLOAD); if (StringUtil.isEmpty(path)) { - mRecordUploadInfo.setText("未设置"); + mRecordUploadInfo.setText(R.string.settings__unset); } else { mRecordUploadInfo.setText(path); } @@ -498,10 +829,27 @@ 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); + mOutputCharsetSettingInfo.setText(SPService.getString(SPService.KEY_OUTPUT_CHARSET, "GBK")); mResolutionSettingWrapper = findViewById(R.id.screenshot_resolution_setting_wrapper); mResolutionSettingInfo = (TextView) findViewById(R.id.screenshot_resolution_setting_info); @@ -509,11 +857,100 @@ private void initView() { mHightlightSettingWrapper = findViewById(R.id.replay_highlight_setting_wrapper); mHightlightSettingInfo = (TextView) findViewById(R.id.replay_highlight_setting_info); - mHightlightSettingInfo.setText(SPService.getBoolean(SPService.KEY_HIGHLIGHT_REPLAY_NODE, true)? "是": "否"); + 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); + if (displaySystemApp) { + mDisplaySystemAppSettingInfo.setText(R.string.constant__yes); + } else { + 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); + if (checkUpdate) { + mCheckUpdateSettingInfo.setText(R.string.constant__yes); + } else { + 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()); mAesSeedSettingWrapper = findViewById(R.id.aes_seed_setting_wrapper); mAesSeedSettingInfo = (TextView) findViewById(R.id.aes_seed_setting_info); - mAesSeedSettingInfo.setText(SPService.getString(SPService.KEY_AES_KEY, "com.alipay.hulu")); + mAesSeedSettingInfo.setText(SPService.getString(SPService.KEY_AES_KEY, AESUtils.DEFAULT_AES_KEY)); mClearFilesSettingWrapper = findViewById(R.id.clear_files_setting_wrapper); mClearFilesSettingInfo = (TextView) findViewById(R.id.clear_files_setting_info); @@ -522,9 +959,9 @@ private void initView() { mHideLogSettingInfo = (TextView) findViewById(R.id.hide_log_setting_info); boolean hideLog = SPService.getBoolean(SPService.KEY_HIDE_LOG, true); if (hideLog) { - mHideLogSettingInfo.setText("是"); + mHideLogSettingInfo.setText(R.string.constant__yes); } else { - mHideLogSettingInfo.setText("否"); + mHideLogSettingInfo.setText(R.string.constant__no); } mImportCaseSettingWrapper = findViewById(R.id.import_case_setting_wrapper); @@ -537,19 +974,139 @@ 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) { + if (resultCode == RESULT_OK) { + String targetFile = data.getStringExtra(FileChooseDialogActivity.KEY_TARGET_FILE); + if (!StringUtil.isEmpty(targetFile)) { + SPService.putString(SPService.KEY_BASE_DIR, targetFile); + mBaseDirSettingInfo.setText(targetFile); + FileUtils.setSolopiBaseDir(targetFile); + } + } + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } + /** * 更新存储的用例 * @param oldSeed * @param newSeed */ private void updateStoredRecords(final String oldSeed, final String newSeed) { - showProgressDialog("开始更新用例"); + showProgressDialog(getString(R.string.settings__start_update_cases)); BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -560,7 +1117,7 @@ public void run() { if (cases != null && cases.size() > 0) { for (int i = 0; i < cases.size(); i++) { - showProgressDialog("更新用例(" + (i + 1) + "/" + cases.size() + ")"); + showProgressDialog(getString(R.string.settings__updating_cases, i + 1, cases.size())); RecordCaseInfo caseInfo = cases.get(i); GeneralOperationLogBean generalOperation; try { @@ -574,6 +1131,10 @@ public void run() { if (generalOperation == null) { continue; } + + // load file content + OperationStepUtil.afterLoad(generalOperation); + List steps = generalOperation.getSteps(); if (generalOperation.getSteps() != null) { for (OperationStep step : steps) { @@ -593,6 +1154,7 @@ public void run() { } } } + OperationStepUtil.beforeStore(generalOperation); // 更新operationLog字段 caseInfo.setOperationLog(JSON.toJSONString(generalOperation)); 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 380c42c..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 @@ -16,7 +16,10 @@ package com.alipay.hulu.activity; import android.Manifest; +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; @@ -34,25 +37,60 @@ public class SplashActivity extends BaseActivity { private Handler handler; + private static final String DISPLAY_ALERT_INFO = "displayAlertInfo"; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); handler = new Handler(); setContentView(R.layout.splash); + + // 如果有自定义目录 + String baseDir = SPService.getString(SPService.KEY_BASE_DIR); + if (!StringUtil.isEmpty(baseDir)) { + FileUtils.setSolopiBaseDir(baseDir); + } + + // 免责弹窗 + boolean showDisplay = SPService.getBoolean(DISPLAY_ALERT_INFO, true); + if (showDisplay) { + AlertDialog dialog = new AlertDialog.Builder(this).setTitle(R.string.index__disclaimer) + .setMessage(R.string.disclaimer) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + processPemission(); + dialog.dismiss(); + } + }).setNegativeButton(R.string.constant__no_inform, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + SPService.putBoolean(DISPLAY_ALERT_INFO, false); + processPemission(); + dialog.dismiss(); + } + }).setCancelable(false).create(); + dialog.setCanceledOnTouchOutside(false); + dialog.show(); + } else { + processPemission(); + } } /** * 写权限后续步骤 */ - 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(); @@ -76,21 +114,23 @@ public void run() { Intent intent = new Intent(SplashActivity.this, IndexActivity.class); startActivity(intent); - SplashActivity.this.finish(); + laterFinish(); } }); } }); } - @Override - protected void onStart() { - super.onStart(); - + 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; } @@ -99,7 +139,7 @@ protected void onStart() { @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() { @@ -110,14 +150,14 @@ public void onPermissionResult(boolean result, String reason) { FileUtils.fallBackToExternalDir(SplashActivity.this); } - afterWritePermission(); + afterWritePermission(false); } }); } } }); } else { - afterWritePermission(); + afterWritePermission(true); } } @@ -137,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 fbab5a1..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,14 +32,33 @@ * 图标 * @return */ - int icon(); + int icon() default -1; + + /** + * 图标ID名称 + * @return + */ + String iconName() default ""; /** * 显示名称 * * @return */ - String name(); + String name() default ""; + + /** + * 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 8098b65..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 @@ -20,8 +20,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; -import android.widget.CheckBox; -import android.widget.CompoundButton; import android.widget.TextView; import com.alipay.hulu.R; @@ -45,7 +43,7 @@ public class BatchExecutionListAdapter extends BaseAdapter{ private Delegate mDelegate; public interface Delegate { - void onItemChecked(boolean isAllSelected); + void onItemAdd(RecordCaseInfo caseInfo); } public BatchExecutionListAdapter(Context context) { @@ -64,34 +62,6 @@ public void updateData(List data) { notifyDataSetChanged(); } - - public void onSelectAllClick(boolean isSelected) { - for (RecordCaseInfo caseInfo : mData) { - caseInfo.setSelected(isSelected); - } - notifyDataSetChanged(); - } - - public boolean isAllSelected() { - for (RecordCaseInfo caseInfo : mData) { - if (!(caseInfo.isSelected())) { - return false; - } - } - return true; - } - - public List getCurrentSelectedCases() { - List targetCases = new ArrayList<>(); - for (RecordCaseInfo caseInfo : mData) { - if (caseInfo.isSelected()) { - targetCases.add(caseInfo); - } - } - - return targetCases; - } - @Override public int getCount() { return mData.size(); @@ -116,7 +86,7 @@ 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.checkbox = (CheckBox) convertView.findViewById(R.id.status); + holder.addBtn = convertView.findViewById(R.id.batch_item_add); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); @@ -129,18 +99,18 @@ 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()); } - holder.checkbox.setOnCheckedChangeListener(null); - holder.checkbox.setChecked(recordCaseInfo.isSelected()); - holder.checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + holder.addBtn.setTag(position); + holder.addBtn.setOnClickListener(new View.OnClickListener() { + @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - recordCaseInfo.setSelected(isChecked); - if (mDelegate != null) { - mDelegate.onItemChecked(isAllSelected()); + public void onClick(View v) { + int position = (int) v.getTag(); + if (position >= 0 && position < mData.size() && mDelegate != null) { + mDelegate.onItemAdd(mData.get(position)); } } }); @@ -153,7 +123,6 @@ static class ViewHolder { TextView caseName; TextView caseDesc; TextView createTime; - CheckBox checkbox; + View addBtn; } - } 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 f5a5616..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,28 +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.util.Pair; 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. */ @@ -50,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 @@ -73,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 @@ -110,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 @@ -174,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; } @@ -213,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/event/RecordCaseChangedEvent.java b/src/app/src/main/java/com/alipay/hulu/bean/CaseRunningParam.java similarity index 50% rename from src/app/src/main/java/com/alipay/hulu/event/RecordCaseChangedEvent.java rename to src/app/src/main/java/com/alipay/hulu/bean/CaseRunningParam.java index 975ef78..d5f76a8 100644 --- a/src/app/src/main/java/com/alipay/hulu/event/RecordCaseChangedEvent.java +++ b/src/app/src/main/java/com/alipay/hulu/bean/CaseRunningParam.java @@ -13,40 +13,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.alipay.hulu.event; +package com.alipay.hulu.bean; -/** - * Created by lezhou.wyl on 2018/8/2. - */ - -public class RecordCaseChangedEvent { +import com.alibaba.fastjson.JSONObject; - public static final int TYPE_LOCAL_DELETE = 1; - public static final int TYPE_SERVER_DELETE = 2; - public static final int TYPE_CASE_ADD = 3; +import java.util.List; - private int type; - private long caseId; +/** + * Created by qiaoruikai on 2019-08-19 21:05. + */ +public class CaseRunningParam { + private ParamMode mode; + private List paramList; - public int getType() { - return type; + public ParamMode getMode() { + return mode; } - public void setType(int type) { - this.type = type; + public void setMode(ParamMode mode) { + this.mode = mode; } - public long getCaseId() { - return caseId; + public List getParamList() { + return paramList; } - public void setCaseId(long caseId) { - this.caseId = caseId; + public void setParamList(List paramList) { + this.paramList = paramList; } - - public RecordCaseChangedEvent(int type, long caseId) { - this.type = type; - this.caseId = caseId; + /** + * 可选模式 + */ + 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/app/src/main/java/com/alipay/hulu/bean/GithubReleaseBean.java b/src/app/src/main/java/com/alipay/hulu/bean/GithubReleaseBean.java new file mode 100644 index 0000000..0abf08f --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/bean/GithubReleaseBean.java @@ -0,0 +1,713 @@ +/* + * 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 java.util.List; + +public class GithubReleaseBean { + + /** + * url : https://api.github.com/repos/alipay/SoloPi/releases/18327760 + * assets_url : https://api.github.com/repos/alipay/SoloPi/releases/18327760/assets + * upload_url : https://uploads.github.com/repos/alipay/SoloPi/releases/18327760/assets{?name,label} + * html_url : https://github.com/alipay/SoloPi/releases/tag/v0.9.0 + * id : 18327760 + * node_id : MDc6UmVsZWFzZTE4MzI3NzYw + * tag_name : v0.9.0 + * target_commitish : master + * name : v0.9.0 + * draft : false + * author : {"login":"soloPi","id":48561756,"node_id":"MDQ6VXNlcjQ4NTYxNzU2","avatar_url":"https://avatars3.githubusercontent.com/u/48561756?v=4","gravatar_id":"","url":"https://api.github.com/users/soloPi","html_url":"https://github.com/soloPi","followers_url":"https://api.github.com/users/soloPi/followers","following_url":"https://api.github.com/users/soloPi/following{/other_user}","gists_url":"https://api.github.com/users/soloPi/gists{/gist_id}","starred_url":"https://api.github.com/users/soloPi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/soloPi/subscriptions","organizations_url":"https://api.github.com/users/soloPi/orgs","repos_url":"https://api.github.com/users/soloPi/repos","events_url":"https://api.github.com/users/soloPi/events{/privacy}","received_events_url":"https://api.github.com/users/soloPi/received_events","type":"User","site_admin":false} + * prerelease : false + * created_at : 2019-07-01T10:11:54Z + * published_at : 2019-07-01T10:30:50Z + * assets : [{"url":"https://api.github.com/repos/alipay/SoloPi/releases/assets/13691817","id":13691817,"node_id":"MDEyOlJlbGVhc2VBc3NldDEzNjkxODE3","name":"Solopi.apk","label":null,"uploader":{"login":"soloPi","id":48561756,"node_id":"MDQ6VXNlcjQ4NTYxNzU2","avatar_url":"https://avatars3.githubusercontent.com/u/48561756?v=4","gravatar_id":"","url":"https://api.github.com/users/soloPi","html_url":"https://github.com/soloPi","followers_url":"https://api.github.com/users/soloPi/followers","following_url":"https://api.github.com/users/soloPi/following{/other_user}","gists_url":"https://api.github.com/users/soloPi/gists{/gist_id}","starred_url":"https://api.github.com/users/soloPi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/soloPi/subscriptions","organizations_url":"https://api.github.com/users/soloPi/orgs","repos_url":"https://api.github.com/users/soloPi/repos","events_url":"https://api.github.com/users/soloPi/events{/privacy}","received_events_url":"https://api.github.com/users/soloPi/received_events","type":"User","site_admin":false},"content_type":"application/vnd.android.package-archive","state":"uploaded","size":3473300,"download_count":476,"created_at":"2019-07-13T10:48:29Z","updated_at":"2019-07-13T10:48:39Z","browser_download_url":"https://github.com/alipay/SoloPi/releases/download/v0.9.0/Solopi.apk"}] + * tarball_url : https://api.github.com/repos/alipay/SoloPi/tarball/v0.9.0 + * zipball_url : https://api.github.com/repos/alipay/SoloPi/zipball/v0.9.0 + * body : tag: v0.9.0 + */ + + private String url; + private String assets_url; + private String upload_url; + private String html_url; + private int id; + private String node_id; + private String tag_name; + private String target_commitish; + private String name; + private boolean draft; + private AuthorBean author; + private boolean prerelease; + private String created_at; + private String published_at; + private String tarball_url; + private String zipball_url; + private String body; + private List assets; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getAssets_url() { + return assets_url; + } + + public void setAssets_url(String assets_url) { + this.assets_url = assets_url; + } + + public String getUpload_url() { + return upload_url; + } + + public void setUpload_url(String upload_url) { + this.upload_url = upload_url; + } + + public String getHtml_url() { + return html_url; + } + + public void setHtml_url(String html_url) { + this.html_url = html_url; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getNode_id() { + return node_id; + } + + public void setNode_id(String node_id) { + this.node_id = node_id; + } + + public String getTag_name() { + return tag_name; + } + + public void setTag_name(String tag_name) { + this.tag_name = tag_name; + } + + public String getTarget_commitish() { + return target_commitish; + } + + public void setTarget_commitish(String target_commitish) { + this.target_commitish = target_commitish; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isDraft() { + return draft; + } + + public void setDraft(boolean draft) { + this.draft = draft; + } + + public AuthorBean getAuthor() { + return author; + } + + public void setAuthor(AuthorBean author) { + this.author = author; + } + + public boolean isPrerelease() { + return prerelease; + } + + public void setPrerelease(boolean prerelease) { + this.prerelease = prerelease; + } + + public String getCreated_at() { + return created_at; + } + + public void setCreated_at(String created_at) { + this.created_at = created_at; + } + + public String getPublished_at() { + return published_at; + } + + public void setPublished_at(String published_at) { + this.published_at = published_at; + } + + public String getTarball_url() { + return tarball_url; + } + + public void setTarball_url(String tarball_url) { + this.tarball_url = tarball_url; + } + + public String getZipball_url() { + return zipball_url; + } + + public void setZipball_url(String zipball_url) { + this.zipball_url = zipball_url; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public List getAssets() { + return assets; + } + + public void setAssets(List assets) { + this.assets = assets; + } + + public static class AuthorBean { + /** + * login : soloPi + * id : 48561756 + * node_id : MDQ6VXNlcjQ4NTYxNzU2 + * avatar_url : https://avatars3.githubusercontent.com/u/48561756?v=4 + * gravatar_id : + * url : https://api.github.com/users/soloPi + * html_url : https://github.com/soloPi + * followers_url : https://api.github.com/users/soloPi/followers + * following_url : https://api.github.com/users/soloPi/following{/other_user} + * gists_url : https://api.github.com/users/soloPi/gists{/gist_id} + * starred_url : https://api.github.com/users/soloPi/starred{/owner}{/repo} + * subscriptions_url : https://api.github.com/users/soloPi/subscriptions + * organizations_url : https://api.github.com/users/soloPi/orgs + * repos_url : https://api.github.com/users/soloPi/repos + * events_url : https://api.github.com/users/soloPi/events{/privacy} + * received_events_url : https://api.github.com/users/soloPi/received_events + * type : User + * site_admin : false + */ + + private String login; + private int id; + private String node_id; + private String avatar_url; + private String gravatar_id; + private String url; + private String html_url; + private String followers_url; + private String following_url; + private String gists_url; + private String starred_url; + private String subscriptions_url; + private String organizations_url; + private String repos_url; + private String events_url; + private String received_events_url; + private String type; + private boolean site_admin; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getNode_id() { + return node_id; + } + + public void setNode_id(String node_id) { + this.node_id = node_id; + } + + public String getAvatar_url() { + return avatar_url; + } + + public void setAvatar_url(String avatar_url) { + this.avatar_url = avatar_url; + } + + public String getGravatar_id() { + return gravatar_id; + } + + public void setGravatar_id(String gravatar_id) { + this.gravatar_id = gravatar_id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getHtml_url() { + return html_url; + } + + public void setHtml_url(String html_url) { + this.html_url = html_url; + } + + public String getFollowers_url() { + return followers_url; + } + + public void setFollowers_url(String followers_url) { + this.followers_url = followers_url; + } + + public String getFollowing_url() { + return following_url; + } + + public void setFollowing_url(String following_url) { + this.following_url = following_url; + } + + public String getGists_url() { + return gists_url; + } + + public void setGists_url(String gists_url) { + this.gists_url = gists_url; + } + + public String getStarred_url() { + return starred_url; + } + + public void setStarred_url(String starred_url) { + this.starred_url = starred_url; + } + + public String getSubscriptions_url() { + return subscriptions_url; + } + + public void setSubscriptions_url(String subscriptions_url) { + this.subscriptions_url = subscriptions_url; + } + + public String getOrganizations_url() { + return organizations_url; + } + + public void setOrganizations_url(String organizations_url) { + this.organizations_url = organizations_url; + } + + public String getRepos_url() { + return repos_url; + } + + public void setRepos_url(String repos_url) { + this.repos_url = repos_url; + } + + public String getEvents_url() { + return events_url; + } + + public void setEvents_url(String events_url) { + this.events_url = events_url; + } + + public String getReceived_events_url() { + return received_events_url; + } + + public void setReceived_events_url(String received_events_url) { + this.received_events_url = received_events_url; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public boolean isSite_admin() { + return site_admin; + } + + public void setSite_admin(boolean site_admin) { + this.site_admin = site_admin; + } + } + + public static class AssetsBean { + /** + * url : https://api.github.com/repos/alipay/SoloPi/releases/assets/13691817 + * id : 13691817 + * node_id : MDEyOlJlbGVhc2VBc3NldDEzNjkxODE3 + * name : Solopi.apk + * label : null + * uploader : {"login":"soloPi","id":48561756,"node_id":"MDQ6VXNlcjQ4NTYxNzU2","avatar_url":"https://avatars3.githubusercontent.com/u/48561756?v=4","gravatar_id":"","url":"https://api.github.com/users/soloPi","html_url":"https://github.com/soloPi","followers_url":"https://api.github.com/users/soloPi/followers","following_url":"https://api.github.com/users/soloPi/following{/other_user}","gists_url":"https://api.github.com/users/soloPi/gists{/gist_id}","starred_url":"https://api.github.com/users/soloPi/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/soloPi/subscriptions","organizations_url":"https://api.github.com/users/soloPi/orgs","repos_url":"https://api.github.com/users/soloPi/repos","events_url":"https://api.github.com/users/soloPi/events{/privacy}","received_events_url":"https://api.github.com/users/soloPi/received_events","type":"User","site_admin":false} + * content_type : application/vnd.android.package-archive + * state : uploaded + * size : 3473300 + * download_count : 476 + * created_at : 2019-07-13T10:48:29Z + * updated_at : 2019-07-13T10:48:39Z + * browser_download_url : https://github.com/alipay/SoloPi/releases/download/v0.9.0/Solopi.apk + */ + + private String url; + private int id; + private String node_id; + private String name; + private Object label; + private UploaderBean uploader; + private String content_type; + private String state; + private int size; + private int download_count; + private String created_at; + private String updated_at; + private String browser_download_url; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getNode_id() { + return node_id; + } + + public void setNode_id(String node_id) { + this.node_id = node_id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Object getLabel() { + return label; + } + + public void setLabel(Object label) { + this.label = label; + } + + public UploaderBean getUploader() { + return uploader; + } + + public void setUploader(UploaderBean uploader) { + this.uploader = uploader; + } + + public String getContent_type() { + return content_type; + } + + public void setContent_type(String content_type) { + this.content_type = content_type; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public int getDownload_count() { + return download_count; + } + + public void setDownload_count(int download_count) { + this.download_count = download_count; + } + + public String getCreated_at() { + return created_at; + } + + public void setCreated_at(String created_at) { + this.created_at = created_at; + } + + public String getUpdated_at() { + return updated_at; + } + + public void setUpdated_at(String updated_at) { + this.updated_at = updated_at; + } + + public String getBrowser_download_url() { + return browser_download_url; + } + + public void setBrowser_download_url(String browser_download_url) { + this.browser_download_url = browser_download_url; + } + + public static class UploaderBean { + /** + * login : soloPi + * id : 48561756 + * node_id : MDQ6VXNlcjQ4NTYxNzU2 + * avatar_url : https://avatars3.githubusercontent.com/u/48561756?v=4 + * gravatar_id : + * url : https://api.github.com/users/soloPi + * html_url : https://github.com/soloPi + * followers_url : https://api.github.com/users/soloPi/followers + * following_url : https://api.github.com/users/soloPi/following{/other_user} + * gists_url : https://api.github.com/users/soloPi/gists{/gist_id} + * starred_url : https://api.github.com/users/soloPi/starred{/owner}{/repo} + * subscriptions_url : https://api.github.com/users/soloPi/subscriptions + * organizations_url : https://api.github.com/users/soloPi/orgs + * repos_url : https://api.github.com/users/soloPi/repos + * events_url : https://api.github.com/users/soloPi/events{/privacy} + * received_events_url : https://api.github.com/users/soloPi/received_events + * type : User + * site_admin : false + */ + + private String login; + private int id; + private String node_id; + private String avatar_url; + private String gravatar_id; + private String url; + private String html_url; + private String followers_url; + private String following_url; + private String gists_url; + private String starred_url; + private String subscriptions_url; + private String organizations_url; + private String repos_url; + private String events_url; + private String received_events_url; + private String type; + private boolean site_admin; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getNode_id() { + return node_id; + } + + public void setNode_id(String node_id) { + this.node_id = node_id; + } + + public String getAvatar_url() { + return avatar_url; + } + + public void setAvatar_url(String avatar_url) { + this.avatar_url = avatar_url; + } + + public String getGravatar_id() { + return gravatar_id; + } + + public void setGravatar_id(String gravatar_id) { + this.gravatar_id = gravatar_id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getHtml_url() { + return html_url; + } + + public void setHtml_url(String html_url) { + this.html_url = html_url; + } + + public String getFollowers_url() { + return followers_url; + } + + public void setFollowers_url(String followers_url) { + this.followers_url = followers_url; + } + + public String getFollowing_url() { + return following_url; + } + + public void setFollowing_url(String following_url) { + this.following_url = following_url; + } + + public String getGists_url() { + return gists_url; + } + + public void setGists_url(String gists_url) { + this.gists_url = gists_url; + } + + public String getStarred_url() { + return starred_url; + } + + public void setStarred_url(String starred_url) { + this.starred_url = starred_url; + } + + public String getSubscriptions_url() { + return subscriptions_url; + } + + public void setSubscriptions_url(String subscriptions_url) { + this.subscriptions_url = subscriptions_url; + } + + public String getOrganizations_url() { + return organizations_url; + } + + public void setOrganizations_url(String organizations_url) { + this.organizations_url = organizations_url; + } + + public String getRepos_url() { + return repos_url; + } + + public void setRepos_url(String repos_url) { + this.repos_url = repos_url; + } + + public String getEvents_url() { + return events_url; + } + + public void setEvents_url(String events_url) { + this.events_url = events_url; + } + + public String getReceived_events_url() { + return received_events_url; + } + + public void setReceived_events_url(String received_events_url) { + this.received_events_url = received_events_url; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public boolean isSite_admin() { + return site_admin; + } + + public void setSite_admin(boolean site_admin) { + this.site_admin = site_admin; + } + } + } +} 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 new file mode 100644 index 0000000..e529bbf --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/fragment/BaseFragment.java @@ -0,0 +1,192 @@ +/* + * 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.Activity; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import android.text.TextUtils; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.Toast; + +import com.alipay.hulu.R; +import com.alipay.hulu.activity.BaseActivity; +import com.alipay.hulu.activity.MyApplication; +import com.alipay.hulu.common.application.LauncherApplication; +import com.alipay.hulu.common.utils.LogUtil; + +public class BaseFragment extends Fragment { + private boolean canShowDialog; + + private static Toast toast; + + private ProgressDialog progressDialog; + + @Override + public void onResume() { + super.onResume(); + } + + @Override + public void onPause() { + super.onPause(); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + long startTime = System.currentTimeMillis(); + + // 主线程等待 + LauncherApplication.getInstance().prepareInMain(); + + LogUtil.w("BaseFragment", "Fragment: %s, 等待Launcher初始化耗时: %dms", + getClass().getSimpleName(), System.currentTimeMillis() - startTime); + } + + protected boolean canShowDialog() { + return canShowDialog; + } + + /** + * 展开软键盘 + */ + public void showInputMethod() { + InputMethodManager imManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imManager.toggleSoftInput(0, InputMethodManager.SHOW_FORCED); + } + + //隐藏输入法 + public void hideSoftInputMethod() { + View view = getActivity().getWindow().peekDecorView(); + if (view != null && view.getWindowToken() != null) { + InputMethodManager imManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + /** + * toast短时间提示 + * + * @param msg + */ + public void toastShort(final String msg) { + if (TextUtils.isEmpty(msg)) { + return; + } + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (toast != null) { + toast.cancel(); + } + toast = Toast.makeText(MyApplication.getContext(), msg, Toast.LENGTH_SHORT); + toast.show(); + } + }); + } + + public void toastShort(String msg, Object... args) { + String formatMsg = String.format(msg, args); + toastShort(formatMsg); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // 绑定下TAG信息 + Activity activity = getActivity(); + if (activity instanceof BaseActivity) { + ((BaseActivity) activity).addFragmentTag(getTag()); + } + } + + /** + * toast长时间提示 + * + * @param msg + */ + public void toastLong(final String msg) { + if (TextUtils.isEmpty(msg)) { + return; + } + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + 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) { + getActivity().runOnUiThread(new Runnable() { + public void run() { + if (progressDialog == null) { + progressDialog = new ProgressDialog(getActivity(), R.style.SimpleDialogTheme); + progressDialog.setMessage(str); + progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + progressDialog.show(); + } else if (progressDialog.isShowing()) { + progressDialog.setMessage(str); + } else { + progressDialog.setMessage(str); + progressDialog.show(); + } + } + }); + } + + public void dismissProgressDialog() { + getActivity().runOnUiThread(new Runnable() { + public void run() { + if (progressDialog != null) { + progressDialog.dismiss(); + } + } + }); + } + + public void updateProgressDialog(final int progress, final int totalProgress, final String message) { + getActivity().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/fragment/BatchExecutionFragment.java b/src/app/src/main/java/com/alipay/hulu/fragment/BatchExecutionFragment.java index 40265bc..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,41 +16,29 @@ package com.alipay.hulu.fragment; import android.os.Bundle; -import android.provider.Settings; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +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.MyApplication; +import com.alipay.hulu.activity.BatchExecutionActivity; 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; /** * Created by lezhou.wyl on 2018/8/19. */ -public class BatchExecutionFragment extends Fragment implements CompoundButton.OnCheckedChangeListener - , BatchExecutionListAdapter.Delegate{ +public class BatchExecutionFragment extends BaseFragment { private static final String TAG = "BatchExeFrag"; private static final String KEY_ARG_FRAGMENT_TYPE = "KEY_ARG_FRAGMENT_TYPE"; @@ -60,8 +48,6 @@ public class BatchExecutionFragment extends Fragment implements CompoundButton.O private View mEmptyView; private TextView mEmptyTextView; private BatchExecutionListAdapter mAdapter; - private CheckBox mSelectAllCheckbox; - private Button mConfirmBtn; private View mContentContainer; public static int[] getTypes() { @@ -69,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) { @@ -103,33 +89,7 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { private void initOtherView(View view) { mContentContainer = view.findViewById(R.id.content_container); - mSelectAllCheckbox = (CheckBox) view.findViewById(R.id.select_all_checkbox); - mConfirmBtn = (Button) view.findViewById(R.id.confirm_btn); - mSelectAllCheckbox.setOnCheckedChangeListener(this); - - mConfirmBtn.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - final List recordCases = mAdapter.getCurrentSelectedCases(); - if (recordCases.size() == 0) { - ((BaseActivity)(getActivity())).toastShort("请选择用例"); - return; - } - - PermissionUtil.OnPermissionCallback callback = new PermissionUtil.OnPermissionCallback() { - @Override - public void onPermissionResult(boolean result, String reason) { - if (result) { - BatchStepProvider provider = new BatchStepProvider(recordCases); - CaseReplayManager manager = LauncherApplication.getInstance().findServiceByName(CaseReplayManager.class.getName()); - manager.start(provider, MyApplication.MULTI_REPLAY_LISTENER); - } - } - }; - checkPermissions(callback); - } - }); } private void getReplayRecordsFromDB() { @@ -166,33 +126,12 @@ private void initListView(View view) { mAdapter = new BatchExecutionListAdapter(getContext()); mListView.setAdapter(mAdapter); - mAdapter.setDelegate(this); + mAdapter.setDelegate((BatchExecutionActivity) getActivity()); } 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(); - } - - private void checkPermissions(PermissionUtil.OnPermissionCallback callback) { - // 高权限,悬浮窗权限判断 - PermissionUtil.requestPermissions(Arrays.asList("adb", Settings.ACTION_ACCESSIBILITY_SETTINGS), getActivity(), callback); - } - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - mAdapter.onSelectAllClick(mSelectAllCheckbox.isChecked()); - } - - @Override - public void onItemChecked(boolean isAllSelected) { - mSelectAllCheckbox.setOnCheckedChangeListener(null); - mSelectAllCheckbox.setChecked(mAdapter.isAllSelected()); - mSelectAllCheckbox.setOnCheckedChangeListener(this); + 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 new file mode 100644 index 0000000..8d68b79 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/fragment/CaseDescEditFragment.java @@ -0,0 +1,141 @@ +/* + * 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.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.shared.io.bean.RecordCaseInfo; + +import java.util.List; + +public class CaseDescEditFragment extends BaseFragment implements CaseEditActivity.OnCaseSaveListener { + private static final String TAG = "CaseStepEditFrag"; + + private RecordCaseInfo mRecordCase; + + private AdvanceCaseSetting setting; + + private EditText mCaseName; + + private EditText mCaseDesc; + private ListView mParams; + + 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初始化 + * + * @param + */ + public static CaseDescEditFragment getInstance(RecordCaseInfo recordCaseInfo) { + CaseDescEditFragment fragment = new CaseDescEditFragment(); + fragment.mRecordCase = recordCaseInfo; + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_case_desc_edit, container, false); + // 获取各项控件 + 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; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + initData(); + } + + /** + * 初始化数据 + */ + private void initData() { + // 如果Intent中没有 + if (mRecordCase == null) { + LogUtil.e(TAG, "There is no record case"); + return; + } + + mCaseName.setText(mRecordCase.getCaseName()); + mCaseDesc.setText(mRecordCase.getCaseDesc()); + setting = JSON.parseObject(mRecordCase.getAdvanceSettings() + , AdvanceCaseSetting.class); + + // 参数列表 + adapter = new ParamListAdapter(getActivity()); + mParams.setAdapter(adapter); + + if (setting != null) { + adapter.setData(setting.getParams()); + } else { + mParams.setVisibility(View.GONE); + } + } + + @Override + public void onCaseSave() { + mRecordCase.setCaseName(mCaseName.getText().toString()); + mRecordCase.setCaseDesc(mCaseDesc.getText().toString()); + + 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); + } + + @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 new file mode 100644 index 0000000..8f167a4 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/fragment/CaseStepEditFragment.java @@ -0,0 +1,1174 @@ +/* + * 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.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +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; +import android.view.ViewGroup; +import android.view.WindowManager; +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.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; +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.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.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; +import com.yydcdut.sdlv.SlideAndDragListView; +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.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.alipay.hulu.shared.node.utils.LogicUtil.SCOPE; + +public class CaseStepEditFragment extends BaseFragment implements TagFlowLayout.OnTagClickListener, CaseEditActivity.OnCaseSaveListener{ + 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; + + private List stepList; + + private String storePath; + + private List dragEntities; + + private AtomicInteger currentIdx; + + private CaseStepAdapter adapter; + + /** + * 通过RecordCase初始化 + * + * @param + */ + public static CaseStepEditFragment getInstance(RecordCaseInfo recordCaseInfo) { + CaseStepEditFragment fragment = new CaseStepEditFragment(); + fragment.recordCase = recordCaseInfo; + return fragment; + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View root = inflater.inflate(R.layout.fragment_case_step_edit, container, false); + // 加载相关控件 + tagGroup = (TagFlowLayout) root.findViewById(R.id.case_step_edit_tag_group); + dragList = (SlideAndDragListView) root.findViewById(R.id.case_step_edit_drag_list); + + LayoutAnimationController controller = new LayoutAnimationController( + AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_in)); + controller.setDelay(0); + dragList.setLayoutAnimation(controller); + return root; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + initData(); + } + + @Subscriber(value = @Param(sticky = false), thread = RunningThread.MAIN_THREAD) + public void onScanEvent(final ScanSuccessEvent event) { + switch (event.getType()) { + case ScanSuccessEvent.SCAN_TYPE_SCHEME: + // 向handler发送请求 + OperationMethod method = new OperationMethod(PerformActionEnum.JUMP_TO_PAGE); + method.putParam(OperationExecutor.SCHEME_KEY, event.getContent()); + OperationStep step = new OperationStep(); + step.setOperationMethod(method); + step.setOperationIndex(currentIdx.get()); + step.setOperationId(stepList.get(stepList.size() - 1).getOperationId()); + + CaseStepAdapter.MyDataWrapper 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_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; + } + } + + /** + * 初始化用例数据 + */ + private void initData() { + if (recordCase == null) { + return; + } + + currentIdx = new AtomicInteger(); + + GeneralOperationLogBean generalOperation = JSON.parseObject(recordCase.getOperationLog(), GeneralOperationLogBean.class); + + // load from file + OperationStepUtil.afterLoad(generalOperation); + storePath = generalOperation.getStorePath(); + + if (generalOperation.getSteps() != null) { + stepList = generalOperation.getSteps(); + } + + // 可以全新添加步骤 + if (stepList == null) { + stepList = new ArrayList<>(); + } + + dragEntities = new ArrayList<>(stepList.size() + 1); + final List stepTags = new ArrayList<>(stepList.size() + 2); + + // 每一步添加一个实体 + 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); + stepTags.add(step.getOperationMethod().getActionEnum().getDesc()); + + String drag = step.getOperationMethod().getParam(SCOPE); + if (drag != null) { + entity.scopeTo = Integer.parseInt(drag) + entity.idx; + } + } + + tagGroup.setMaxSelectCount(0); + tagGroup.setAdapter(new TagAdapter(stepTags) { + @Override + public View getView(FlowLayout parent, int position, String o) { + View tag = LayoutInflater.from(getActivity()).inflate(R.layout.item_case_step_tag, null); + TextView title = (TextView) tag.findViewById(R.id.case_step_edit_tag_title); + ImageView icon = (ImageView) tag.findViewById(R.id.case_step_edit_tag_icon); + + if (position == 0) { + 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 - 4); + PerformActionEnum actionEnum = step.getOperationMethod().getActionEnum(); + + // 设置资源 + title.setText(actionEnum.getDesc()); + icon.setImageResource(actionEnum.getIcon()); + } + return tag; + } + }); + tagGroup.setOnTagClickListener(this); + + // 用例adapter + adapter = new CaseStepAdapter(getActivity(), dragEntities); + adapter.setCurrentMode(selectMode); + + // 设置菜单相关样式 + 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.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.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(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(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()); + + // 空项 + 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, 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 == 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()); + } + adapter.notifyDataSetChanged(); + + return Menu.ITEM_SCROLL_BACK; + } + return 0; + } + }); + + dragList.setOnDragDropListener(adapter); + dragList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + showEditDialog(dragEntities.get(position)); + } + }); + } + + /** + * 切换选择模式 + */ + 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) { + 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 - 4); + CaseStepAdapter.MyDataWrapper entity = new CaseStepAdapter.MyDataWrapper(clone(step), currentIdx.getAndIncrement()); + + // 如果是if和while,需要设置为0 + OperationMethod method = step.getOperationMethod(); + if (method.getActionEnum() == PerformActionEnum.IF || method.getActionEnum() == PerformActionEnum.WHILE) { + entity.scopeTo = 0; + } + + dragEntities.add(entity); + // 添加用例步骤 + adapter.notifyDataSetChanged(); + + return true; + } + + /** + * 将scopeTo信息存储到param中 + */ + private void saveScopeInfo() { + for (int i = 0; i < dragEntities.size(); i++) { + CaseStepAdapter.MyDataWrapper wrapper = dragEntities.get(i); + + if (wrapper.scopeTo > -1) { + for (int j = i + 1; j < dragEntities.size(); j++) { + CaseStepAdapter.MyDataWrapper to = dragEntities.get(j); + + if (to.idx == wrapper.scopeTo) { + wrapper.currentStep.getOperationMethod().putParam(SCOPE, Integer.toString(j - i)); + break; + } + } + } + } + } + + 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信息 + saveScopeInfo(); + + List operations = new ArrayList<>(dragEntities.size() + 1); + for (CaseStepAdapter.MyDataWrapper wrapper: dragEntities) { + operations.add(wrapper.currentStep); + } + + GeneralOperationLogBean logBean = new GeneralOperationLogBean(); + logBean.setSteps(operations); + logBean.setStorePath(storePath); + OperationStepUtil.beforeStore(logBean); + + recordCase.setOperationLog(JSON.toJSONString(logBean)); + } + + /** + * 显示编辑框 + * @param wrapper + */ + private void showEditDialog(final CaseStepAdapter.MyDataWrapper wrapper) { + final View v = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_case_step_edit, null); + final RecyclerView r = (RecyclerView) v.findViewById(R.id.dialog_case_step_edit_recycler); + MaxHeightScrollView scroll = (MaxHeightScrollView) v.findViewById(R.id.dialog_case_step_edit_scroll); + DisplayMetrics dm = new DisplayMetrics(); + ((WindowManager) LauncherApplication.getInstance().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getMetrics(dm); + final int height = dm.heightPixels; + scroll.setMaxHeight(height / 2); + + // 拷贝一份 + final OperationStep clone = clone(wrapper.currentStep); + r.setLayoutManager(new LinearLayoutManager(getActivity())); + + final CaseStepNodeAdapter nodeAdapter; + final TabLayout tab = (TabLayout) v.findViewById(R.id.dialog_case_step_edit_tab); + if (clone.getOperationNode() != null) { + TabLayout.Tab tabItem = tab.newTab(); + 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(R.string.step_edit__method_info); + tab.addTab(tabItem, 0); + + // 配置后续列表 + final List laterList; + int curPos = dragEntities.indexOf(wrapper); + if (wrapper.scopeTo > -1) { + laterList = dragEntities.subList(curPos + 1, dragEntities.size()); + + boolean flag = false; + for (int pos = curPos + 1; pos < dragEntities.size(); pos++) { + if (dragEntities.get(pos).idx == wrapper.scopeTo) { + clone.getOperationMethod().putParam(SCOPE, Integer.toString(pos - curPos)); + flag = true; + break; + } + } + if (!flag) { + clone.getOperationMethod().putParam(SCOPE, "1"); + } + } else { + laterList = new ArrayList<>(); + } + + final CaseStepMethodAdapter paramAdapter = new CaseStepMethodAdapter(laterList, + clone.getOperationMethod()); + + tab.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + if (StringUtil.equals(tab.getText(), getString(R.string.step_edit__node_info))) { + r.setAdapter(nodeAdapter); + } else { + r.setAdapter(paramAdapter); + } + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + + } + + @Override + 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) { + if (which == Dialog.BUTTON_POSITIVE) { + wrapper.currentStep = clone; + + // 如果需要配置scopeTo + if (wrapper.scopeTo > -1) { + int pos = Integer.parseInt(clone.getOperationMethod().getParam(SCOPE)) - 1; + wrapper.scopeTo = laterList.get(pos).idx; + } + + adapter.notifyDataSetChanged(); + } + } + }; + + final AlertDialog dialog = new AlertDialog.Builder(getActivity()) + .setView(v).setPositiveButton(R.string.constant__confirm, dialogClick) + .setNegativeButton(R.string.constant__cancel, dialogClick) + .setTitle(clone.getOperationMethod().getActionEnum().getDesc()).create(); + dialog.show(); + + // 选择第一个 + dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + } + + /** + * 基于Parcel拷贝一份数据 + * @param origin + * @return + */ + private OperationStep clone(OperationStep origin) { + Parcel p = Parcel.obtain(); + origin.writeToParcel(p, 0); + p.setDataPosition(0); + return OperationStep.CREATOR.createFromParcel(p); + } + + /** + * 展示选择添加步骤模式 + */ + 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 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 onClick(DialogInterface dialog, int which) { + OperationNode node = new OperationNode(); + String classNameText = className.getText().toString(); + if (StringUtil.isEmpty(classNameText)) { + classNameText = "*"; + } + node.setClassName(classNameText); + + 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); + + String xpathText = xpath.getText().toString(); + if (StringUtil.isEmpty(xpathText)) { + xpathText = null; + } + node.setXpath(xpathText); + + if (dialog != null) { + dialog.dismiss(); + } + + // 默认使用辅助功能控件 + 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); + } + + /** + * 显示添加操作界面 + */ + 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); + step.setOperationNode(node); + + CaseStepAdapter.MyDataWrapper wrapper = new CaseStepAdapter.MyDataWrapper(step, currentIdx.getAndIncrement()); + dragEntities.add(position, wrapper); + adapter.notifyDataSetChanged(); + } + + @Override + public void onCancel() { + + } + }); + } 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; + } + } + + 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); + } + + 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); + } + + + adapter.notifyDataSetChanged(); + } + + @Override + public void onCancel() { + + } + }); + } + } + + 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 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(R.string.function_group__device); + GLOBAL_ICONS.add(R.drawable.dialog_action_drawable_device_operation); + List gDeviceActions = new ArrayList<>(); + gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.BACK)); + gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.HOME)); + gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.HANDLE_ALERT)); + gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.SCREENSHOT)); + gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.SLEEP)); + gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.EXECUTE_SHELL)); + gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.NOTIFICATION)); + gDeviceActions.add(convertPerformActionToSubMenu(PerformActionEnum.RECENT_TASK)); + GLOBAL_ACTION_MAP.put(R.string.function_group__device, gDeviceActions); + + 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(R.string.function_group__app, gAppActions); + + 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)); + 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(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)); + gLoopActions.add(convertPerformActionToSubMenu(PerformActionEnum.LOAD_PARAM)); + GLOBAL_ACTION_MAP.put(R.string.function_group__logic, gLoopActions); + } + + /** + * 转换为菜单 + * @param actionEnum + * @return + */ + private static TwoLevelSelectLayout.SubMenuItem convertPerformActionToSubMenu(PerformActionEnum actionEnum) { + 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 d7cb519..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,9 +19,9 @@ import android.content.Intent; import android.os.Bundle; import android.provider.Settings; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -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; @@ -37,13 +37,13 @@ import com.alibaba.fastjson.JSON; import com.alipay.hulu.R; import com.alipay.hulu.activity.CaseEditActivity; -import com.alipay.hulu.activity.CaseStepEditActivity; +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; @@ -52,21 +52,17 @@ 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.event.RecordCaseChangedEvent; -import com.alipay.hulu.replay.OperationStepProvider; -import com.alipay.hulu.replay.RepeatStepProvider; -import com.alipay.hulu.service.CaseReplayManager; +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.db.GreenDaoManager; import com.alipay.hulu.shared.io.db.RecordCaseInfoDao; +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 org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; @@ -79,7 +75,7 @@ * Created by lezhou.wyl on 2018/7/30. */ -public class ReplayListFragment extends Fragment { +public class ReplayListFragment extends BaseFragment { private static final String TAG = "ReplayListFrag"; private static final String KEY_ARG_FRAGMENT_TYPE = "KEY_ARG_FRAGMENT_TYPE"; @@ -89,6 +85,8 @@ public class ReplayListFragment extends Fragment { private View mEmptyView; private TextView mEmptyTextView; private ReplayListAdapter mAdapter; + private SwipeRefreshLayout refreshLayout; + private String app; public static ReplayListFragment newInstance(int type) { return new ReplayListFragment(); @@ -99,17 +97,20 @@ public static int[] getAvailableTypes() { } public static String getTypeName(int type) { - return "本地"; + return StringUtil.getString(R.string.replay_list__local); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - EventBus.getDefault().register(this); - 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) { @@ -122,18 +123,18 @@ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { initEmptyView(view); initListView(view); - getReplayRecordsFromDB(); + getReplayRecordsFromDB(null); } /** * 重载下用例 */ - @Subscriber(value = @Param(value = NewRecordActivity.NEED_REFRESH_CASES_LIST, sticky = false)) - public void reloadCases() { - getReplayRecordsFromDB(); + @Subscriber(value = @Param(value = NewRecordActivity.NEED_REFRESH_LOCAL_CASES_LIST, sticky = false)) + public void reloadLocalCases() { + getReplayRecordsFromDB(null); } - private void getReplayRecordsFromDB() { + private void getReplayRecordsFromDB(final Runnable r) { BackgroundExecutor.execute(new Runnable() { @Override public void run() { @@ -143,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); @@ -158,30 +162,29 @@ public void run() { } private void initListView(View view) { - mListView = (ListView) view.findViewById(R.id.replay_list); - mAdapter = new ReplayListAdapter(getContext()); - - mListView.setAdapter(mAdapter); - - // 设置编辑按键监听器 - mAdapter.setOnEditClickListener(new AdapterView.OnItemClickListener() { + refreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.replay_swipe_refresh); + refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - RecordCaseInfo caseInfo = (RecordCaseInfo) mAdapter.getItem(position); - if (caseInfo == null) { - return; - } + public void onRefresh() { + Runnable r = new Runnable() { + @Override + public void run() { + refreshLayout.setRefreshing(false); + } + }; - // 启动编辑页 - Intent intent = new Intent(getActivity(), CaseEditActivity.class); - int caseId = CaseStepHolder.storeCase(caseInfo); - intent.putExtra(CaseEditActivity.RECORD_CASE_EXTRA, caseId); - startActivity(intent); + // 读取用例 + getReplayRecordsFromDB(r); } }); - // 默认点击播放 - mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + mListView = (ListView) view.findViewById(R.id.replay_list); + mAdapter = new ReplayListAdapter(getContext()); + + mListView.setAdapter(mAdapter); + + // 设置播放按键监听器 + mAdapter.setOnPlayClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { final RecordCaseInfo caseInfo = (RecordCaseInfo) mAdapter.getItem(position); @@ -193,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()); } } @@ -205,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) { @@ -223,8 +230,8 @@ public void onExecute(DialogInterface dialog, PerformActionEnum action) { case PLAY_MULTI_TIMES: repeatPrepare(position); break; - case EDIT_CASE: - editCase(position); + case GEN_MULTI_PARAM: + genMultiParams(position); break; } } @@ -245,13 +252,33 @@ public void onDismiss(DialogInterface dialog) { }); } + /** + * 编辑用例描述信息 + * @param position + */ private void editCase(int position) { - Intent intent = new Intent(getActivity(), CaseStepEditActivity.class); + RecordCaseInfo caseInfo = (RecordCaseInfo) mAdapter.getItem(position); + if (caseInfo == null) { + return; + } + caseInfo = caseInfo.clone(); + // 启动编辑页 + Intent intent = new Intent(getActivity(), CaseEditActivity.class); + int caseId = CaseStepHolder.storeCase(caseInfo); + intent.putExtra(CaseEditActivity.RECORD_CASE_EXTRA, caseId); + startActivity(intent); + } - RecordCaseInfo caseInfo = ((RecordCaseInfo) mAdapter.getItem(position)).clone(); - // 暂存 - int id = CaseStepHolder.storeCase(caseInfo); - intent.putExtra("case", id); + 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); } @@ -262,22 +289,38 @@ private void editCase(int position) { private void deleteCase(final int position) { AlertDialog dialog = new AlertDialog.Builder(getContext(), R.style.SimpleDialogTheme) .setCancelable(false) - .setMessage("删除此用例?") - .setPositiveButton("确定", new DialogInterface.OnClickListener() { + .setMessage(R.string.replay__delete_case) + .setPositiveButton(R.string.constant__confirm, new DialogInterface.OnClickListener() { @Override - public void onClick(DialogInterface dialog, int which) { + public void onClick(final DialogInterface dialog, int which) { final RecordCaseInfo recordCaseInfo = (RecordCaseInfo) mAdapter.getItem(position); if (recordCaseInfo == null) { return; } - GreenDaoManager.getInstance().getRecordCaseInfoDao().deleteByKey(recordCaseInfo.getId()); - mAdapter.deleteCase(recordCaseInfo); - EventBus.getDefault().post(new RecordCaseChangedEvent( - RecordCaseChangedEvent.TYPE_LOCAL_DELETE, recordCaseInfo.getId())); - dialog.dismiss(); + // delete step file + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + String operationLog = recordCaseInfo.getOperationLog(); + if (!StringUtil.isEmpty(operationLog)) { + GeneralOperationLogBean logBean = JSON.parseObject(operationLog, GeneralOperationLogBean.class); + if (logBean != null && !StringUtil.isEmpty(logBean.getStorePath())) { + File steps = new File(logBean.getStorePath()); + if (steps.exists()) { + FileUtils.deleteFile(steps); + } + } + } + GreenDaoManager.getInstance().getRecordCaseInfoDao().deleteByKey(recordCaseInfo.getId()); + InjectorService.g().pushMessage(NewRecordActivity.NEED_REFRESH_LOCAL_CASES_LIST); + + dialog.dismiss(); + } + }); + } - }).setNegativeButton("取消", new DialogInterface.OnClickListener() { + }).setNegativeButton(R.string.constant__cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); @@ -296,10 +339,16 @@ private void exportCase(int position) { if (caseInfo == null) { return; } - BackgroundExecutor.execute(new Runnable() { @Override public void run() { + // 读取实际用例信息 + String operationLog = caseInfo.getOperationLog(); + GeneralOperationLogBean logBean = JSON.parseObject(operationLog, GeneralOperationLogBean.class); + OperationStepUtil.afterLoad(logBean); + logBean.setStorePath(null); + caseInfo.setOperationLog(JSON.toJSONString(logBean)); + String content = JSON.toJSONString(caseInfo); // 导出文件 @@ -332,9 +381,9 @@ 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("开始执行", new DialogInterface.OnClickListener() { + .setPositiveButton(R.string.constant__start_execution, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { LogUtil.i(TAG, "Positive " + which); @@ -398,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()); } } @@ -412,7 +457,7 @@ public void onPermissionResult(final boolean result, String reason) { private void initEmptyView(View view) { mEmptyView = view.findViewById(R.id.empty_view_container); mEmptyTextView = (TextView) view.findViewById(R.id.empty_text); - mEmptyTextView.setText("您还没有录制用例哦"); + mEmptyTextView.setText(R.string.record__no_local_case); } /** @@ -439,12 +484,7 @@ public void run() { @Override public void onDestroy() { - EventBus.getDefault().unregister(this); + InjectorService.g().unregister(this); super.onDestroy(); } - - @Subscribe(threadMode = ThreadMode.MAIN) - public void onCaseDeleted(RecordCaseChangedEvent event) { - - } } 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 0988c4d..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 @@ -35,6 +35,7 @@ public class BatchStepProvider extends AbstractStepProvider { private int currentCaseIdx; private OperationStep prepareStep; + private boolean restart; OperationStepProvider currentStepProvider; @@ -46,9 +47,10 @@ public void prepare() { loadProvider(currentCaseIdx); } - public BatchStepProvider(List recordCaseInfos) { + public BatchStepProvider(List recordCaseInfos, boolean restart) { mRecordCases = recordCaseInfos; currentCaseIdx = 0; + this.restart = restart; resultBeans = new ArrayList<>(recordCaseInfos.size() + 1); } @@ -73,8 +75,11 @@ private void loadProvider(int startPos) { currentStepProvider = new OperationStepProvider(currentCase); - prepareStep = new OperationStep(); - prepareStep.setOperationMethod(new OperationMethod(PerformActionEnum.GOTO_INDEX)); + // 重启应用 + if (restart) { + prepareStep = new OperationStep(); + prepareStep.setOperationMethod(new OperationMethod(PerformActionEnum.GOTO_INDEX)); + } currentStepProvider.prepare(); } @@ -107,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 e55a3e9..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,22 +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; @@ -45,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; @@ -54,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<>(); @@ -62,6 +75,8 @@ public class OperationStepProvider extends AbstractStepProvider { protected Map screenshotFiles; protected String targetApp; + protected String targetAppPkg; + protected String targetAppVersionName; protected RecordCaseInfo caseInfo; @@ -81,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) { @@ -115,6 +242,13 @@ public void loadOperation(String content) { return; } GeneralOperationLogBean generalOperation = JSON.parseObject(content, GeneralOperationLogBean.class); + if (generalOperation == null) { + return; + } + + // load from file + OperationStepUtil.afterLoad(generalOperation); + if (generalOperation.getSteps() != null) { stepList.addAll(generalOperation.getSteps()); } @@ -133,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) { @@ -175,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; @@ -203,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,直接跳出去 @@ -237,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; @@ -290,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, "节点未查找到"))) { @@ -305,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); @@ -315,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; } @@ -323,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); @@ -342,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 d3e3b0c..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,30 +92,32 @@ 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; import com.alipay.hulu.util.FunctionSelectUtil; import com.theartofdev.edmodo.cropper.CropImageView; -import org.greenrobot.eventbus.EventBus; - 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; @@ -109,8 +125,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static android.accessibilityservice.AccessibilityService.GESTURE_SWIPE_UP; - /** * 操作录制服务 * @@ -125,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; @@ -136,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; @@ -166,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); + } } }; @@ -213,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(); } @@ -228,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; @@ -240,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恢复 @@ -255,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(); @@ -281,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 @@ -300,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() @@ -308,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); + } } + } }); } @@ -328,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); } @@ -344,38 +485,488 @@ 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); + } + } + + /** + * 手势监听器 + */ + 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, "收到空触摸消息"); + 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) { + 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); + + // 看下是否点到SoloPi图标 + if (binder.checkInFloat(point)) { + 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 receiveTouchPosition(UniversalEventBean eventBean) { - Point point = eventBean.getParam(com.alipay.hulu.shared.event.constant.Constant.KEY_TOUCH_POINT); +// @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, "收到空触摸消息【%s】", eventBean); + LogUtil.w(TAG, "收到空触摸消息"); return; } @@ -385,22 +976,23 @@ 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) { 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; } @@ -422,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; } @@ -434,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) { @@ -458,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; } /** @@ -479,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); @@ -499,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(), @@ -526,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) { @@ -546,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() { @@ -555,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)); @@ -579,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)); @@ -621,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); } /** @@ -666,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; @@ -701,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(); @@ -717,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(); @@ -737,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(); @@ -755,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(); @@ -766,6 +1392,8 @@ public void run() { return; } + // 先切换到默认输入法 + CmdTools.switchToIme(defaultIme); // 处理方法 FunctionSelectUtil.showFunctionView(context, node, keys, icons, secondLevel, highLightService, operationService, getLocalClickPos(), new FunctionSelectUtil.FunctionListener() { @@ -773,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 @@ -796,7 +1427,7 @@ public void onCancel() { ((CaptureTree) node).resetBound(); } - setServiceToTouchBlockMode(); + setServiceToTouchBlockModeNoDelay(); notifyDialogDismiss(); } }); @@ -843,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; @@ -850,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)) @@ -940,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"); @@ -963,8 +1659,9 @@ public void onDestroy(Context context) { listener = null; stopListener = null; + operationService.stopExtraActionHandle(); + injectorService.unregister(this); - EventBus.getDefault().unregister(this); } /** @@ -993,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()) { @@ -1021,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; } @@ -1056,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(); @@ -1074,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); @@ -1131,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; @@ -1150,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 @@ -1193,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 a1ff6e4..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,9 +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; @@ -48,9 +52,10 @@ 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; import com.alipay.hulu.common.utils.FileUtils; @@ -71,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; @@ -80,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; /** * 操作回放服务 @@ -96,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"; /** @@ -118,6 +130,11 @@ public class CaseReplayManager implements ExportService { */ private OperationService operationService; + /** + * 操作服务 + */ + private TouchService touchService; + /** * 高亮服务 */ @@ -135,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); + } + }); /** * 目标应用 @@ -164,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; } @@ -179,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(); } /** @@ -206,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); @@ -229,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); + } } /** @@ -249,6 +314,8 @@ public void onDestroy(Context context) { binder = null; } + operationService.stopExtraActionHandle(); + runningExecutor.shutdownNow(); LauncherApplication.getInstance().stopServiceByName(HighLightService.class.getName()); } @@ -261,7 +328,7 @@ public void startProcess() { binder.hideFloat(); runningFlag = true; - runningExecutor.execute(new Runnable() { + final Runnable runningR = new Runnable() { @Override public void run() { try { @@ -270,78 +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(); - // 执行各步骤 - 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; + } + + /** + * 单步操作 + * @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; + } - if (result != null) { - LogUtil.e(TAG, "执行步骤出现问题:%s", result); + LogUtil.i(TAG, "开始执行操作:%s", step); + updateFloatIcon(R.drawable.solopi_running); - boolean isError = provider.reportErrorStep(step, result); + String result; + try { + result = processOperation(step); + } catch (Exception e) { + LogUtil.e(TAG, "执行操作抛出异常: " + e.getMessage(), e); + result = "执行异常:" + e.getMessage(); + } - if (StringUtil.equals(result, "回放中止")) { - break; - } + // 是否阻塞执行 + boolean isError = result != null; + if (isError) { + LogUtil.e(TAG, "执行步骤出现问题:%s", result); + isError = provider.reportErrorStep(step, result, new ArrayList()); + } - // 如果是error步骤 - if (isError) { - break; + // 有需要监听执行结果的监听器 + 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.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(); @@ -355,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)); } /** @@ -393,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 { @@ -404,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) { @@ -428,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) { @@ -442,7 +671,7 @@ public void notifyOperationFinish() { // 没找到 if (node == null) { - LogUtil.w(TAG, "未查找到节点【%s】", operation.getOperationNode()); + LogUtil.w(TAG, "未查找到节点【%s】", origin); return "节点未查找到"; } } catch (NumberFormatException e) { @@ -450,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 "节点未查找到"; } } @@ -484,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); @@ -504,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; } @@ -608,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()) { @@ -658,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(); @@ -730,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 17d58a3..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 @@ -17,18 +17,24 @@ import android.content.Context; import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import com.alipay.hulu.R; 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 static final float middle_title_size = 20f; + private LinearLayout headMenuLayout; public HeadControlPanel(Context context, AttributeSet attrs) { super(context, attrs); @@ -41,6 +47,7 @@ protected void onFinishInflate() { mMidleTitle = (TextView)findViewById(R.id.midle_title); infoIcon = (ImageView) findViewById(R.id.info_icon); backIcon = (ImageView) findViewById(R.id.back_icon); + headMenuLayout = (LinearLayout) findViewById(R.id.head_info_menu_layout); backIcon.setVisibility(GONE); infoIcon.setVisibility(GONE); @@ -49,9 +56,106 @@ protected void onFinishInflate() { super.onFinishInflate(); } - public void setMiddleTitle(String s){ + /** + * 左侧添加菜单 + * @param v + */ + public void addMenuFromLeft(View v) { + ViewGroup.LayoutParams params = v.getLayoutParams(); + LinearLayout.LayoutParams real; + if (params != null) { + if (params instanceof LinearLayout.LayoutParams) { + real = (LinearLayout.LayoutParams) params; + } else { + real = new LinearLayout.LayoutParams(params); + } + } else { + real = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + } + + 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); + } + + /** + * 右侧添加菜单 + * @param v + */ + public void addMenuFromRight(View v) { + ViewGroup.LayoutParams params = v.getLayoutParams(); + LinearLayout.LayoutParams real; + if (params != null) { + if (params instanceof LinearLayout.LayoutParams) { + real = (LinearLayout.LayoutParams) params; + } else { + real = new LinearLayout.LayoutParams(params); + } + } else { + real = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + } + + 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) { @@ -60,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 bf0ec83..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 @@ -15,14 +15,19 @@ */ package com.alipay.hulu.upgrade; +import android.app.Activity; import android.util.Pair; +import com.alipay.hulu.R; +import com.alipay.hulu.activity.BaseActivity; +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.ClassUtil; import com.alipay.hulu.common.utils.DeviceInfoUtil; import com.alipay.hulu.common.utils.HttpUtil; import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.MiscUtil; import com.alipay.hulu.common.utils.PatchProcessUtil; import com.alipay.hulu.common.utils.StringUtil; import com.alipay.hulu.common.utils.patch.PatchLoadResult; @@ -31,8 +36,11 @@ 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; @@ -58,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)) { @@ -66,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); @@ -76,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(); + } } }); } @@ -89,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; @@ -101,6 +117,8 @@ private static void doUpgradePatch(PatchResponse response) { return; } + final AtomicInteger loadingCount = new AtomicInteger(0); + Map> patchMap = new HashMap<>(); for (final PatchResponse.DataBean data : patches) { @@ -118,25 +136,31 @@ private static void doUpgradePatch(PatchResponse response) { if (result != null && result.version < data.getVersion()) { LogUtil.i(TAG, "强制升级插件: %s, 版本号from %f to %f", data.getName(), result.version, data.getVersion()); + loadingCount.incrementAndGet(); BackgroundExecutor.execute(new Runnable() { @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) { LogUtil.e(TAG, "下载插件失败"); + LauncherApplication.getInstance().showToast(StringUtil.getString(R.string.patch__load_failed, data.getName())); + loadingCount.decrementAndGet(); 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); } + loadingCount.decrementAndGet(); } }); } @@ -148,25 +172,30 @@ public void run() { if (result == null || result.version < data.getVersion()) { LogUtil.i(TAG, "安装基础依赖插件: %s, 版本号:%f", data.getName(), data.getVersion()); + loadingCount.incrementAndGet(); BackgroundExecutor.execute(new Runnable() { @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) { LogUtil.e(TAG, "下载插件失败"); + loadingCount.decrementAndGet(); 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); } + loadingCount.decrementAndGet(); } }); } @@ -174,7 +203,36 @@ public void run() { } + if (loadingCount.get() > 0) { + final BaseActivity activity = (BaseActivity) LauncherApplication.getInstance().loadActivityOnTop(); + if (activity != null) { + activity.showProgressDialog(StringUtil.getString(R.string.patch__loading_plugin_wait)); + BackgroundExecutor.execute(new Runnable() { + @Override + public void run() { + while (!loadingCount.compareAndSet(0, 0)) { + MiscUtil.sleep(5); + } + activity.dismissProgressDialog(); + } + }); + } + } + // 更新本地插件版本 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 edcdc71..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 @@ -17,6 +17,7 @@ import com.alibaba.fastjson.JSON; import com.alipay.hulu.common.bean.DeviceInfo; +import com.alipay.hulu.common.service.SPService; import com.alipay.hulu.common.utils.DeviceInfoUtil; import com.alipay.hulu.common.utils.FileUtils; import com.alipay.hulu.common.utils.HttpUtil; @@ -27,8 +28,13 @@ import java.io.BufferedWriter; import java.io.File; +import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; @@ -45,6 +51,7 @@ */ public class RecordUtil { private static final String TAG = "RecordUtil"; + private static final DateFormat TIME_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA); /** * 保存到文件夹 @@ -66,6 +73,16 @@ public static File saveToFile(Map> // 保存目录 File saveFolder = loadSaveDir(startTime, endTime); + // 加载编码信息 + String charsetName = SPService.getString(SPService.KEY_OUTPUT_CHARSET, "GBK"); + Charset charset; + try { + charset = Charset.forName(charsetName); + } catch (UnsupportedCharsetException e) { + LogUtil.w(TAG, "unsupported charset for name=" + charsetName, e); + charset = Charset.forName("UTF-8"); + } + for (Map.Entry> entry: records.entrySet()){ RecordPattern pattern = entry.getKey(); @@ -73,15 +90,16 @@ public static File saveToFile(Map> File saveFile = new File(saveFolder, pattern.getName() + "_" + pattern.getSource() + "_" + pattern.getStartTime() + "_" + pattern.getEndTime() + ".csv"); try { if (saveFile.createNewFile()) { - BufferedWriter writer = new BufferedWriter(new FileWriter(saveFile)); + 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(); @@ -105,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/java/com/alipay/hulu/util/UpgradeUtil.java b/src/app/src/main/java/com/alipay/hulu/util/UpgradeUtil.java new file mode 100644 index 0000000..4a53032 --- /dev/null +++ b/src/app/src/main/java/com/alipay/hulu/util/UpgradeUtil.java @@ -0,0 +1,107 @@ +/* + * 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 com.alipay.hulu.BuildConfig; +import com.alipay.hulu.bean.GithubReleaseBean; +import com.alipay.hulu.common.utils.HttpUtil; +import com.alipay.hulu.common.utils.LogUtil; +import com.alipay.hulu.common.utils.StringUtil; + +import java.io.IOException; + +import okhttp3.Call; + +public class UpgradeUtil { + private static final String RELEASE_API = "https://api.github.com/repos/alipay/solopi/releases/latest"; + private static final String TAG = "UpgradeUtil"; + + /** + * Check update + * @param listener + */ + public static void checkForUpdate(final CheckUpdateListener listener) { + // no upgrade for debug version + if (BuildConfig.DEBUG) { + return; + } + + HttpUtil.get(RELEASE_API, new HttpUtil.Callback(GithubReleaseBean.class) { + @Override + public void onResponse(Call call, GithubReleaseBean item) throws IOException { + String versionName = SystemUtil.getAppVersionName(); + String tagName = item.getTag_name(); + if (StringUtil.startWith(tagName, "v")) { + tagName = tagName.substring(1); + } + + // should update? + boolean update = shouldUpdate(tagName, versionName); + if (update) { + listener.onNewUpdate(item); + } else { + listener.onNoUpdate(); + } + } + + @Override + public void onFailure(Call call, IOException e) { + LogUtil.e(TAG, "Check update failed", e); + listener.onUpdateFailed(e); + } + }); + } + + /** + * Check versions + * @param newVersion + * @param oldVersion + * @return + */ + private static boolean shouldUpdate(String newVersion, String oldVersion) { + if (StringUtil.isEmpty(newVersion) || StringUtil.isEmpty(oldVersion)) { + return false; + } + + try { + String[] newSplit = newVersion.split("\\."); + String[] oldSplit = oldVersion.split("\\."); + int commonLength = Math.min(newSplit.length, oldSplit.length); + for (int i = 0; i < commonLength; i++) { + int newCode = Integer.parseInt(newSplit[i]); + int oldCode = Integer.parseInt(oldSplit[i]); + if (newCode > oldCode) { + return true; + } else if (newCode < oldCode) { + return false; + } + } + + return newSplit.length > oldSplit.length; + } catch (NumberFormatException e) { + LogUtil.e(TAG, e, "parse version failed: old: %s, new: %s", oldVersion, newVersion); + return false; + } + } + + public interface CheckUpdateListener { + void onNoUpdate(); + + void onNewUpdate(GithubReleaseBean release); + + void onUpdateFailed(Throwable t); + } +} diff --git a/src/app/src/main/res/drawable-xhdpi/icon_save.png b/src/app/src/main/res/drawable-xhdpi/icon_save.png deleted file mode 100644 index 1bd3eaa..0000000 Binary files a/src/app/src/main/res/drawable-xhdpi/icon_save.png and /dev/null differ 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/batch_execute_remove.xml b/src/app/src/main/res/drawable/batch_execute_remove.xml new file mode 100644 index 0000000..a9c94cb --- /dev/null +++ b/src/app/src/main/res/drawable/batch_execute_remove.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable/bg_confirm_btn.xml b/src/app/src/main/res/drawable/bg_confirm_btn.xml index 22e6641..c7eb9b8 100644 --- a/src/app/src/main/res/drawable/bg_confirm_btn.xml +++ b/src/app/src/main/res/drawable/bg_confirm_btn.xml @@ -20,7 +20,7 @@ android:radius="4dp"/> + android:color="@color/colorPrimaryDark"/> @@ -30,7 +30,7 @@ android:radius="4dp"/> + android:color="@color/colorPrimary"/> 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_save.xml b/src/app/src/main/res/drawable/icon_save.xml new file mode 100644 index 0000000..871df27 --- /dev/null +++ b/src/app/src/main/res/drawable/icon_save.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file 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 @@ + + + + + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable/primary_button_layer.xml b/src/app/src/main/res/drawable/primary_button_layer.xml new file mode 100644 index 0000000..4612517 --- /dev/null +++ b/src/app/src/main/res/drawable/primary_button_layer.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/src/app/src/main/res/drawable/round_rect_mask.xml b/src/app/src/main/res/drawable/round_rect_mask.xml index 9386f9a..b7e7a4f 100644 --- a/src/app/src/main/res/drawable/round_rect_mask.xml +++ b/src/app/src/main/res/drawable/round_rect_mask.xml @@ -1,4 +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 7815ebb..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,19 +22,94 @@ 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_weight="1" + android:layout_height="0dp"> + + + + + + + + + + + + + + + + + +