├── .gitignore ├── LICENSE ├── README.md ├── README_CN.md ├── apk ├── DroidCast-debug-1.2.1.apk └── checksum256.txt ├── apk_src_path.png ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── rayworks │ │ └── droidcast │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── aidl │ │ └── android │ │ │ └── view │ │ │ ├── IRotationWatcher.aidl │ │ │ └── IWindowManager.aidl │ ├── java │ │ └── com │ │ │ └── rayworks │ │ │ └── droidcast │ │ │ ├── DisplayUtil.java │ │ │ ├── ImageFormat.java │ │ │ ├── Main.java │ │ │ ├── MainActivity.java │ │ │ ├── ScreenCaptorUtils.java │ │ │ ├── TextUtils.kt │ │ │ └── wrapper │ │ │ └── DisplayControl.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── rayworks │ └── droidcast │ └── ExampleUnitTest.java ├── artifacts.gradle ├── build.gradle ├── cast.png ├── cmd_tool ├── .gitignore ├── LICENSE ├── cmd_runner.c ├── makefile ├── tests │ ├── README.md │ ├── meson.build │ └── util_test.c └── utils │ ├── error_printer.c │ ├── error_printer.h │ ├── string_util.c │ └── string_util.h ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── payment_cn.jpg ├── process_main.png ├── screen_shot_dock.png ├── script-rs ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src │ └── main.rs ├── scripts ├── README.md ├── auto_connector.py ├── automation.py ├── automation3.py └── screencap_server.py ├── settings.gradle └── web ├── README.md ├── image.html ├── styles └── layout.css └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | 10 | .vscode 11 | node_modules/ 12 | package-lock.json 13 | package.json 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![DroidCast](./cast.png) 2 | 3 | # DroidCast 4 | 5 | [中文文档](/README_CN.md) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rayworks/droidcast) 6 | 7 | An experimental demo for capturing and displaying screenshot of Android devices in the WebBrowser. 8 | It's compatible with Android OS 4.0+ and could be used as a convenient tool for taking a batch of 9 | screenshots by different image formats && dimensions, reporting bugs and reviewing app features. 10 | 11 | ⚠️ Some hidden methods have been called via reflection which may break as Android OS evolves. 12 | 13 | ![](./screen_shot_dock.png) 14 | 15 | ## Dependencies 16 | 17 | * [Python](https://www.python.org/downloads/) 18 | * [ADB tool](https://developer.android.com/studio/releases/platform-tools) 19 | 20 | ## Quick start 21 | 22 | * Connect your device/emulator 23 | * Install the apk 24 | 25 | Download and install the prebuilt apk from [here](./apk/DroidCast-debug-1.2.1.apk) or install it directly: 26 | 27 | ./gradlew clean installDebug 28 | 29 | * Run the script (using `automation3.py` for Python 3.6+) 30 | 31 | 32 | 33 | python scripts/automation.py 34 | 35 | After that, the default web browser will be opened. You should see the screenshot now. 36 | 37 | * (Optional) Specify the target connected device and set the port of ScreenShot service 38 | 39 | 40 | 41 | python scripts/automation.py -p 12346 -s 'your-device-id' 42 | 43 | For more info, `python scripts/automation.py -h` 44 | 45 | ## Use it wirelessly 46 | 47 | * Get your device IP address (in Settings - System - About phone - Status) e.g : `192.168.x.x` 48 | * Enable adb over TCP/IP on your device: `adb tcpip 5555` 49 | * Connect to your device: `adb connect 192.168.x.x:5555` (replace `192.168.x.x` with the actual IP address) 50 | * Unplug your device 51 | * Go through all the steps under [Common usage](#usage) 52 | 53 | To switch back to USB mode: `adb usb`. 54 | 55 |

Common usage:

56 | 57 | ### Note 58 | 59 | Once apk file installed, you can use the [python scripts](/scripts/automation.py) to automate the following `adb` related operations. 60 | 61 | * Install the apk properly on the phone (Don't install it via `Run 'app'` from Android Studio). 62 | 63 | 64 | 65 | ./gradlew clean installDebug 66 | 67 | * Push the apk to the `tmp` folder 68 | 69 | 70 | 71 | adb push ${your-project-path}/DroidCast/app/build/outputs/apk/debug/DroidCast-debug-1.0.apk /data/local/tmp 72 | 73 | * Start our internal server process for image processing by `app_process` 74 | 75 | 76 | 77 | $ adb shell 78 | D1C:/ $ export CLASSPATH=/data/local/tmp/DroidCast-debug-1.0.apk 79 | D1C:/ $ exec app_process /system/bin com.rayworks.droidcast.Main '$@' 80 | >>> DroidCast main entry 81 | 82 | ![](/process_main.png) 83 | 84 | * Please note: On some devices, 85 | if you got the error "appproc: ERROR: could not find class 'com.rayworks.droidcast.Main', please replace the 86 | above value of `CLASSPATH` with the result returned by `adb shell pm path com.rayworks.droidcast`. 87 | 88 | ![](/apk_src_path.png) 89 | 90 | * Use `adb` forward socket connection from your pc to the connected device 91 | 92 | 93 | 94 | $ adb forward tcp:53516 tcp:53516 95 | 96 | * View the image via web browser 97 | http://localhost:53516/screenshot or with the specific dimension and image format, 98 | e.g. http://localhost:53516/screenshot?format=png\&width=1080\&height=1920 99 | 100 | Currently `png`, `jpeg` and `webp`, these image types are supported. 101 | 102 | ## Reference
103 | 104 | [vysor 原理以及 Android 同屏方案](https://juejin.im/entry/57fe39400bd1d00058dd4652) 105 | 106 | [scrcpy : Display and control your Android device](https://github.com/Genymobile/scrcpy) 107 | 108 | ## Alternatives 109 | 110 | [scrcpy](https://github.com/Genymobile/scrcpy) 111 | 112 | [web-adb](https://github.com/mfinkle/web-adb) 113 | 114 | [AndroidScreenShot\_SysApi](https://github.com/weizongwei5/AndroidScreenShot_SysApi) 115 | 116 | ## Donation 117 |
Buy me a coffee ☕️ 118 | 119 |
120 | 121 |
122 | 123 |
124 | 125 | ## Stargazers over time 126 | 127 | [![Stargazers over time](https://starchart.cc/rayworks/DroidCast.svg)](https://starchart.cc/rayworks/DroidCast) 128 | 129 | ## License 130 | 131 | Copyright (C) 2018 rayworks 132 | 133 | Licensed under the Apache License, Version 2.0 (the "License"); 134 | you may not use this file except in compliance with the License. 135 | You may obtain a copy of the License at 136 | 137 | http://www.apache.org/licenses/LICENSE-2.0 138 | 139 | Unless required by applicable law or agreed to in writing, software 140 | distributed under the License is distributed on an "AS IS" BASIS, 141 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 142 | See the License for the specific language governing permissions and 143 | limitations under the License. 144 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | ![DroidCast](./cast.png) 2 | 3 | # DroidCast 4 | 5 | 在 Android 设备上截屏并在浏览器上显示屏幕图像的一个实验性项目。该工具能兼容 Android 6 | 4.0 及以上系统版本。它支持按照多种图片格式和指定大小实时截屏,是批量截图,上报bug以及 7 | 演示 app 功能页面的首选工具。 8 | 9 | ⚠️ 代码中通过反射调用了一些系统隐藏的方法,相关功能可能会随着 Android 系统接口的变化而受到影响. 10 | 11 | ![](./screen_shot_dock.png) 12 | 13 | ## 依赖 14 | 15 | * [Python](https://www.python.org/downloads/) 16 | * [ADB tool](https://developer.android.google.cn/studio/releases/platform-tools) 17 | 18 | ## 快速上手 19 | 20 | * 连接你的 Android 设备或是模拟器 21 | * 安装 Apk 22 | 23 | 从 [这里](./apk/DroidCast-debug-1.2.1.apk) 下载并安装应用,或是直接通过命令行安装: 24 | 25 | ./gradlew clean installDebug 26 | 27 | * 运行脚本 (Python3.6 及以上环境请使用 `automation3.py`) 28 | 29 | 30 | 31 | python scripts/automation.py 32 | 33 | 然后,默认浏览器将会打开,你就能看到截屏了。 34 | 35 | * (可选) 指定已连接的单台设备以及设置截屏服务端口 36 | 37 | 38 | 39 | python scripts/automation.py -p 12346 -s 'your-device-id' 40 | 41 | 详细请查看 `python scripts/automation.py -h` 42 | 43 | ## 同网段 WIFI 环境下无线配置使用: 44 | 45 | * 获取设备 IP 地址 (设置-系统-关于手机-状态) e.g : `192.168.x.x` 46 | * 在设备上配置启用 adb 的 TCP/IP 模式 : `adb tcpip 5555` 47 | * 连接你的设备 : `adb connect 192.168.x.x:5555`(替换`192.168.x.x`为设备实际 IP) 48 | * 拔掉 USB 连接线 49 | * 按下面的 [一般(USB 连线)使用说明](#usage) 中的步骤完成设置 50 | 51 | 如需重置为默认的 USB 模式,请执行 : `adb usb` 52 | 53 |

一般(USB 连线)使用说明:

54 | 55 | ### 备注 56 | 57 | 一旦 APK 安装完成,你可以使用 [python 脚本](/scripts/automation.py) 来自动化完成以下 `adb` 命令相关的操作. 58 | 59 | * 执行以下命令在手机上安装 apk (不要通过 Android Studio `Run 'app'` 来安装) 60 | 61 | 62 | 63 | ./gradlew clean installDebug 64 | 65 | * 将 apk 文件 push 到手机的 `tmp` 文件夹下 66 | 67 | 68 | 69 | adb push ${your-project-path}/DroidCast/app/build/outputs/apk/debug/DroidCast-debug-1.0.apk /data/local/tmp 70 | 71 | * 通过 `app_process` 启动内部的图片处理服务进程 72 | 73 | 74 | 75 | $ adb shell 76 | D1C:/ $ export CLASSPATH=/data/local/tmp/DroidCast-debug-1.0.apk 77 | D1C:/ $ exec app_process /system/bin com.rayworks.droidcast.Main '$@' 78 | >>> DroidCast main entry 79 | 80 | ![](/process_main.png) 81 | 82 | * 请注意: 在某些设备上, 如果你碰到类似 "appproc: ERROR: could not find class 'com.rayworks.droidcast.Main'的错误, 83 | 请把上面 shell 命令中的 `CLASSPATH` 值替换为执行命令 `adb shell pm path com.rayworks.droidcast` 后的实际返回值。 84 | 85 | ![](/apk_src_path.png) 86 | 87 | * 使用 `adb forward` 命令将本地(PC)`socket` 连接重定向到远端已连接的设备(手机)上。 88 | 89 | 90 | 91 | $ adb forward tcp:53516 tcp:53516 92 | 93 | * 在 PC 上打开浏览器查看截图 94 | http://localhost:53516/screenshot 95 | 或者按指定的图片大小和类型查看,如 96 | http://localhost:53516/screenshot?format=png\&width=1080\&height=1920 97 | 目前支持的图片类型有`png`, `jpeg` 以及 `webp`。 98 | 99 | ## 参考 100 | 101 | [vysor 原理以及 Android 同屏方案](https://juejin.im/entry/57fe39400bd1d00058dd4652) 102 | 103 | [scrcpy : Display and control your Android device](https://github.com/Genymobile/scrcpy) 104 | 105 | ## 相关项目 106 | 107 | [scrcpy](https://github.com/Genymobile/scrcpy) 108 | 109 | [web-adb](https://github.com/mfinkle/web-adb) 110 | 111 | [AndroidScreenShot\_SysApi](https://github.com/weizongwei5/AndroidScreenShot_SysApi) 112 | 113 | ## 赞助开发 114 |
请我喝杯咖啡 ☕️ 115 | 116 |
117 | 118 |
119 | 120 |
121 | 122 | ## License 123 | 124 | Copyright (C) 2018 rayworks 125 | 126 | Licensed under the Apache License, Version 2.0 (the "License"); 127 | you may not use this file except in compliance with the License. 128 | You may obtain a copy of the License at 129 | 130 | http://www.apache.org/licenses/LICENSE-2.0 131 | 132 | Unless required by applicable law or agreed to in writing, software 133 | distributed under the License is distributed on an "AS IS" BASIS, 134 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 135 | See the License for the specific language governing permissions and 136 | limitations under the License. 137 | -------------------------------------------------------------------------------- /apk/DroidCast-debug-1.2.1.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/apk/DroidCast-debug-1.2.1.apk -------------------------------------------------------------------------------- /apk/checksum256.txt: -------------------------------------------------------------------------------- 1 | 8e62cad9389d2663de10746c3380823fd7a282ed61e208178e0f745d182af8d7 ./DroidCast-debug-1.2.1.apk 2 | -------------------------------------------------------------------------------- /apk_src_path.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/apk_src_path.png -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdk 34 6 | defaultConfig { 7 | applicationId "com.rayworks.droidcast" 8 | minSdkVersion 14 9 | targetSdkVersion 34 10 | versionCode 131 11 | versionName "1.2.1" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | 21 | compileOptions { 22 | sourceCompatibility JavaVersion.VERSION_17 23 | targetCompatibility JavaVersion.VERSION_17 24 | } 25 | 26 | buildFeatures { 27 | aidl true 28 | } 29 | namespace 'com.rayworks.droidcast' 30 | } 31 | 32 | dependencies { 33 | implementation fileTree(dir: 'libs', include: ['*.jar']) 34 | 35 | implementation 'androidx.appcompat:appcompat:1.6.1' 36 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 37 | implementation 'androidx.core:core-ktx:1.10.0' 38 | 39 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 40 | 41 | implementation 'com.koushikdutta.async:androidasync:3.1.0' 42 | 43 | testImplementation 'junit:junit:4.13.2' 44 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 45 | 46 | androidTestImplementation("androidx.test:runner:1.5.2") 47 | androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', { 48 | exclude group: 'com.android.support', module: 'support-annotations' 49 | }) 50 | } 51 | 52 | apply from: "../artifacts.gradle" 53 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/seanzhou/Documents/sean/android-sdk-mac/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/rayworks/droidcast/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast; 2 | 3 | import android.content.Context; 4 | import androidx.test.platform.app.InstrumentationRegistry; 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 23 | 24 | assertEquals("com.rayworks.droidcast", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/aidl/android/view/IRotationWatcher.aidl: -------------------------------------------------------------------------------- 1 | package android.view; 2 | 3 | interface IRotationWatcher { 4 | void onRotationChanged(int rotation); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/aidl/android/view/IWindowManager.aidl: -------------------------------------------------------------------------------- 1 | package android.view; 2 | 3 | import android.graphics.Point; 4 | import android.view.IRotationWatcher; 5 | 6 | /*** 7 | * Define this same aidl file here to make it compiling when calling system hidden APIs. 8 | * 9 | * According to the 'delegation model' for loading classes on Java plattform, the actual 10 | * implementation of IWindowManager from the Framework will be applied. 11 | */ 12 | interface IWindowManager { 13 | 14 | void getInitialDisplaySize(int displayId, out Point size); 15 | 16 | void getBaseDisplaySize(int displayId, out Point size); 17 | 18 | void getRealDisplaySize(out Point paramPoint); 19 | 20 | /** 21 | * Remove a rotation watcher set using watchRotation. 22 | * @hide 23 | */ 24 | void removeRotationWatcher(IRotationWatcher watcher); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/rayworks/droidcast/DisplayUtil.java: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Matrix; 7 | import android.graphics.Point; 8 | import android.os.Build; 9 | import android.os.IBinder; 10 | import android.os.RemoteException; 11 | import android.view.Display; 12 | import android.view.IRotationWatcher; 13 | import android.view.IWindowManager; 14 | 15 | import java.lang.reflect.Method; 16 | 17 | /** Created by Sean on 5/27/17. */ 18 | /* package */ final class DisplayUtil { 19 | 20 | private IWindowManager iWindowManager; 21 | 22 | @SuppressLint("PrivateApi") 23 | public DisplayUtil() { 24 | Class serviceManagerClass = null; 25 | 26 | try { 27 | serviceManagerClass = Class.forName("android.os.ServiceManager"); 28 | 29 | Method getService = serviceManagerClass.getDeclaredMethod("getService", String.class); 30 | 31 | // WindowManager 32 | Object ws = getService.invoke(null, Context.WINDOW_SERVICE); 33 | 34 | iWindowManager = IWindowManager.Stub.asInterface((IBinder) ws); 35 | 36 | } catch (Exception e) { 37 | e.printStackTrace(); 38 | } 39 | } 40 | 41 | /** 42 | * * Retrieves the device actual display size. 43 | * 44 | * @return {@link Point} 45 | */ 46 | Point getCurrentDisplaySize() { 47 | try { 48 | Point localPoint = new Point(); 49 | 50 | // Resolve the screen resolution for devices with OS version 4.3+ 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { 52 | iWindowManager.getInitialDisplaySize(0, localPoint); 53 | } else { 54 | // void getDisplaySize(out Point size) 55 | Point out = new Point(); 56 | 57 | iWindowManager 58 | .getClass() 59 | .getMethod("getDisplaySize", Point.class) 60 | .invoke(iWindowManager, out); 61 | if (out.x > 0 && out.y > 0) { 62 | localPoint = out; 63 | } 64 | } 65 | 66 | System.out.println(">>> Dimension: " + localPoint); 67 | return localPoint; 68 | 69 | } catch (Exception e) { 70 | e.printStackTrace(); 71 | } 72 | return null; 73 | } 74 | 75 | /** 76 | * Retrieve the current orientation of the primary screen. 77 | * 78 | * @return Constant as per {@link android.view.Surface.Rotation}. 79 | * @see android.view.Display#DEFAULT_DISPLAY 80 | */ 81 | int getScreenRotation() { 82 | int rotation = 0; 83 | 84 | try { 85 | Class cls = iWindowManager.getClass(); 86 | try { 87 | rotation = (Integer) cls.getMethod("getRotation").invoke(iWindowManager); 88 | } catch (NoSuchMethodException e) { 89 | rotation = 90 | (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(iWindowManager); 91 | } 92 | } catch (Exception e) { 93 | e.printStackTrace(); 94 | } 95 | System.out.println(">>> Screen rotation: " + rotation); 96 | 97 | return rotation; 98 | } 99 | 100 | void setRotateListener(RotateListener listener) { 101 | try { 102 | Class clazz = iWindowManager.getClass(); 103 | 104 | IRotationWatcher watcher = 105 | new IRotationWatcher.Stub() { 106 | @Override 107 | public void onRotationChanged(int rotation) throws RemoteException { 108 | if (listener != null) { 109 | listener.onRotate(rotation); 110 | } 111 | } 112 | }; 113 | 114 | try { 115 | clazz.getMethod("watchRotation", IRotationWatcher.class, int.class) 116 | .invoke(iWindowManager, watcher, Display.DEFAULT_DISPLAY); // 26+ 117 | 118 | } catch (NoSuchMethodException ex) { 119 | clazz.getMethod("watchRotation", IRotationWatcher.class) 120 | .invoke(iWindowManager, watcher); 121 | } 122 | 123 | } catch (Exception e) { 124 | e.printStackTrace(); 125 | } 126 | } 127 | 128 | Bitmap rotateBitmap(Bitmap bitmap, float degree) { 129 | Matrix matrix = new Matrix(); 130 | matrix.postRotate(degree); 131 | 132 | return Bitmap.createBitmap( 133 | bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); 134 | } 135 | 136 | interface RotateListener { 137 | void onRotate(int rotate); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app/src/main/java/com/rayworks/droidcast/ImageFormat.java: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast; 2 | 3 | import androidx.annotation.NonNull; 4 | import android.text.TextUtils; 5 | 6 | public enum ImageFormat { 7 | UNKNOWN(""), 8 | PNG("png"), 9 | JPEG("jpeg"), 10 | WEBP("webp"); 11 | private final String value; 12 | 13 | ImageFormat(String value) { 14 | this.value = value; 15 | } 16 | 17 | @NonNull 18 | public static ImageFormat resolveFormat(String format) { 19 | if (TextUtils.isEmpty(format)) { 20 | return UNKNOWN; 21 | } 22 | 23 | for (ImageFormat fmt : values()) { 24 | if (fmt.value.equalsIgnoreCase(format)) { 25 | return fmt; 26 | } 27 | } 28 | 29 | return UNKNOWN; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/rayworks/droidcast/Main.java: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Point; 5 | import android.os.Build; 6 | import android.os.Handler; 7 | import android.os.Looper; 8 | import android.os.Process; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | import androidx.core.util.Pair; 13 | 14 | import android.text.TextUtils; 15 | 16 | import com.koushikdutta.async.AsyncServer; 17 | import com.koushikdutta.async.http.Multimap; 18 | import com.koushikdutta.async.http.WebSocket; 19 | import com.koushikdutta.async.http.server.AsyncHttpServer; 20 | import com.koushikdutta.async.http.server.AsyncHttpServerRequest; 21 | import com.koushikdutta.async.http.server.AsyncHttpServerResponse; 22 | import com.koushikdutta.async.http.server.HttpServerRequestCallback; 23 | 24 | import org.json.JSONException; 25 | import org.json.JSONObject; 26 | 27 | import java.io.ByteArrayOutputStream; 28 | import java.io.IOException; 29 | import java.util.Locale; 30 | 31 | /** 32 | * Created by seanzhou on 3/14/17. 33 | */ 34 | public class Main { 35 | private static final String sTAG = Main.class.getName(); 36 | 37 | private static final String IMAGE_JPEG = "image/jpeg"; 38 | private static final String IMAGE_WEBP = "image/webp"; 39 | private static final String IMAGE_PNG = "image/png"; 40 | private static final String WIDTH = "width"; 41 | private static final String HEIGHT = "height"; 42 | private static final String FORMAT = "format"; 43 | private static final int SCREENSHOT_DELAY_MILLIS = 1500; 44 | 45 | private static Looper looper; 46 | private static int width = 0; 47 | private static int height = 0; 48 | 49 | private static int port = 53516; 50 | 51 | private static DisplayUtil displayUtil; 52 | private static Handler handler; 53 | 54 | public static void main(String[] args) { 55 | resolveArgs(args); 56 | 57 | AsyncHttpServer httpServer = 58 | new AsyncHttpServer() { 59 | @Override 60 | protected boolean onRequest( 61 | AsyncHttpServerRequest request, AsyncHttpServerResponse response) { 62 | return super.onRequest(request, response); 63 | } 64 | }; 65 | 66 | Looper.prepareMainLooper(); 67 | 68 | looper = Looper.myLooper(); 69 | System.out.println(">>> DroidCast main entry"); 70 | 71 | handler = new Handler(looper); 72 | 73 | displayUtil = new DisplayUtil(); 74 | 75 | AsyncServer server = new AsyncServer(); 76 | httpServer.get("/screenshot", new AnyRequestCallback()); 77 | 78 | httpServer.websocket( 79 | "/src", 80 | (webSocket, request) -> { 81 | 82 | Pair pair = getDimension(); 83 | displayUtil.setRotateListener( 84 | rotate -> { 85 | System.out.println(">>> rotate to " + rotate); 86 | 87 | // delay for the new rotated screen 88 | handler.postDelayed( 89 | () -> { 90 | Pair dimen = getDimension(); 91 | sendScreenshotData(webSocket, dimen.first, dimen.second); 92 | }, 93 | SCREENSHOT_DELAY_MILLIS); 94 | }); 95 | 96 | sendScreenshotData(webSocket, pair.first, pair.second); 97 | }); 98 | 99 | httpServer.listen(server, port); 100 | 101 | Looper.loop(); 102 | } 103 | 104 | private static void resolveArgs(String[] args) { 105 | if (args.length > 0) { 106 | String[] params = args[0].split("="); 107 | 108 | if ("--port".equals(params[0])) { 109 | try { 110 | port = Integer.parseInt(params[1]); 111 | System.out.println(sTAG + " | Port set to " + port); 112 | } catch (NumberFormatException e) { 113 | e.printStackTrace(); 114 | } 115 | } 116 | } 117 | } 118 | 119 | @NonNull 120 | private static Pair getDimension() { 121 | Point displaySize = displayUtil.getCurrentDisplaySize(); 122 | 123 | int width = 1080; 124 | int height = 1920; 125 | if (displaySize != null) { 126 | width = displaySize.x; 127 | height = displaySize.y; 128 | } 129 | return new Pair<>(width, height); 130 | } 131 | 132 | private static void sendScreenshotData(WebSocket webSocket, int width, int height) { 133 | try { 134 | byte[] inBytes = 135 | getScreenImageInBytes( 136 | Bitmap.CompressFormat.JPEG, 137 | width, 138 | height, 139 | (w, h, rotation) -> { 140 | JSONObject obj = new JSONObject(); 141 | try { 142 | obj.put("width", w); 143 | obj.put("height", h); 144 | obj.put("rotation", rotation); 145 | 146 | webSocket.send(obj.toString()); 147 | } catch (JSONException e) { 148 | e.printStackTrace(); 149 | } 150 | }); 151 | webSocket.send(inBytes); 152 | 153 | } catch (IOException e) { 154 | e.printStackTrace(); 155 | } 156 | } 157 | 158 | private static byte[] getScreenImageInBytes( 159 | Bitmap.CompressFormat compressFormat, 160 | int w, 161 | int h, 162 | @Nullable ImageDimensionListener resolver) 163 | throws IOException { 164 | 165 | int screenRotation = displayUtil.getScreenRotation(); 166 | if (screenRotation != 0 && screenRotation != 2) { // not portrait 167 | int len = w; 168 | w = h; 169 | h = len; 170 | } 171 | 172 | int destWidth = w; 173 | int destHeight = h; 174 | Bitmap bitmap = ScreenCaptorUtils.screenshot(destWidth, destHeight); 175 | 176 | if (bitmap == null) { 177 | System.out.printf( 178 | Locale.ENGLISH, 179 | ">>> failed to generate image with resolution %d:%d%n", 180 | Main.width, 181 | Main.height); 182 | 183 | destWidth /= 2; 184 | destHeight /= 2; 185 | 186 | bitmap = ScreenCaptorUtils.screenshot(destWidth, destHeight); 187 | } 188 | 189 | System.out.printf( 190 | Locale.ENGLISH, 191 | "Bitmap generated with resolution %d:%d, process id %d | thread id %d%n", 192 | destWidth, 193 | destHeight, 194 | Process.myPid(), 195 | Process.myTid()); 196 | 197 | int width = bitmap.getWidth(); 198 | int height = bitmap.getHeight(); 199 | System.out.println("Bitmap final dimens : " + width + "|" + height); 200 | if (resolver != null) { 201 | resolver.onResolveDimension(width, height, screenRotation); 202 | } 203 | 204 | ByteArrayOutputStream bout = new ByteArrayOutputStream(); 205 | bitmap.compress(compressFormat, 100, bout); 206 | bout.flush(); 207 | 208 | // "Make sure to call Bitmap.recycle() as soon as possible, once its content is not 209 | // needed anymore." 210 | bitmap.recycle(); 211 | 212 | return bout.toByteArray(); 213 | } 214 | 215 | interface ImageDimensionListener { 216 | void onResolveDimension(int width, int height, int rotation); 217 | } 218 | 219 | static class AnyRequestCallback implements HttpServerRequestCallback { 220 | private Pair mapRequestFormatInfo(ImageFormat imageFormat) { 221 | Bitmap.CompressFormat compressFormat; 222 | String contentType; 223 | 224 | switch (imageFormat) { 225 | case JPEG: 226 | compressFormat = Bitmap.CompressFormat.JPEG; 227 | contentType = IMAGE_JPEG; 228 | break; 229 | case PNG: 230 | compressFormat = Bitmap.CompressFormat.PNG; 231 | contentType = IMAGE_PNG; 232 | break; 233 | case WEBP: 234 | compressFormat = Bitmap.CompressFormat.WEBP; 235 | contentType = IMAGE_WEBP; 236 | break; 237 | 238 | default: 239 | throw new UnsupportedOperationException("Unsupported image format detected"); 240 | } 241 | 242 | return new Pair<>(compressFormat, contentType); 243 | } 244 | 245 | @Nullable 246 | private Pair getImageFormatInfo(String reqFormat) { 247 | 248 | ImageFormat format = ImageFormat.JPEG; 249 | 250 | if (!TextUtils.isEmpty(reqFormat)) { 251 | ImageFormat imageFormat = ImageFormat.resolveFormat(reqFormat); 252 | if (ImageFormat.UNKNOWN.equals(imageFormat)) { 253 | return null; 254 | } else { 255 | // default format 256 | format = imageFormat; 257 | } 258 | } 259 | 260 | return mapRequestFormatInfo(format); 261 | } 262 | 263 | @Override 264 | public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { 265 | try { 266 | Multimap pairs = request.getQuery(); 267 | 268 | String width = pairs.getString(WIDTH); 269 | String height = pairs.getString(HEIGHT); 270 | String reqFormat = pairs.getString(FORMAT); 271 | 272 | Pair formatInfo = getImageFormatInfo(reqFormat); 273 | 274 | if (formatInfo == null) { 275 | response.code(400); 276 | response.send( 277 | String.format( 278 | Locale.ENGLISH, "Unsupported image format : %s", reqFormat)); 279 | 280 | return; 281 | } 282 | 283 | if (!TextUtils.isEmpty(width) && !TextUtils.isEmpty(height) && 284 | TextUtils.isDigitsOnly(width) && TextUtils.isDigitsOnly(height)) { 285 | Main.width = Integer.parseInt(width); 286 | Main.height = Integer.parseInt(height); 287 | } 288 | 289 | if (Main.width == 0 || Main.height == 0) { 290 | // dimension initialization 291 | Point point = displayUtil.getCurrentDisplaySize(); 292 | 293 | if (point != null && point.x > 0 && point.y > 0) { 294 | Main.width = point.x; 295 | Main.height = point.y; 296 | } else { 297 | Main.width = 480; 298 | Main.height = 800; 299 | } 300 | } 301 | 302 | int destWidth = Main.width; 303 | int destHeight = Main.height; 304 | 305 | byte[] bytes = getScreenImageInBytes(formatInfo.first, destWidth, destHeight, null); 306 | 307 | response.send(formatInfo.second, bytes); 308 | 309 | } catch (Exception e) { 310 | e.printStackTrace(); 311 | 312 | response.code(500); 313 | String template = ":( Failed to generate the screenshot on device / emulator : %s - %s - Android OS : %s"; 314 | String error = String.format(Locale.ENGLISH, template, Build.MANUFACTURER, Build.DEVICE, Build.VERSION.RELEASE); 315 | response.send(error); 316 | } 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /app/src/main/java/com/rayworks/droidcast/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast; 2 | 3 | import android.content.pm.ApplicationInfo; 4 | import android.os.Bundle; 5 | import android.widget.TextView; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | public class MainActivity extends AppCompatActivity { 10 | 11 | @Override 12 | protected void onCreate(Bundle savedInstanceState) { 13 | super.onCreate(savedInstanceState); 14 | setContentView(R.layout.activity_main); 15 | 16 | /*** 17 | * https://developer.android.google.cn/about/versions/oreo/android-8.0-changes.html 18 | * #security-all 19 | */ 20 | ApplicationInfo info = getApplicationInfo(); 21 | String srcLocation = info.sourceDir; 22 | 23 | TextView textView = findViewById(R.id.text); 24 | textView.setText(TextUtils.Companion.format("Source apk Dir:", srcLocation)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/rayworks/droidcast/ScreenCaptorUtils.java: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.graphics.Bitmap; 5 | import android.graphics.ColorSpace; 6 | import android.graphics.Rect; 7 | import android.hardware.HardwareBuffer; 8 | import android.os.Build; 9 | import android.os.IBinder; 10 | 11 | import androidx.annotation.RequiresApi; 12 | 13 | import com.rayworks.droidcast.wrapper.DisplayControl; 14 | 15 | import java.lang.reflect.Constructor; 16 | import java.lang.reflect.InvocationTargetException; 17 | import java.lang.reflect.Method; 18 | 19 | /** 20 | * Created by seanzhou on 3/14/17. 21 | */ 22 | @SuppressLint({"BlockedPrivateApi", "PrivateApi"}) 23 | public final class ScreenCaptorUtils { 24 | 25 | private static final String METHOD_SCREENSHOT = "screenshot"; 26 | 27 | private static final Class clazz; 28 | private static Method getBuiltInDisplayMethod; 29 | 30 | static { 31 | try { 32 | String className; 33 | int sdkInt = Build.VERSION.SDK_INT; 34 | if (sdkInt >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 35 | className = "android.window.ScreenCapture"; 36 | } else if (sdkInt > Build.VERSION_CODES.JELLY_BEAN_MR1) { 37 | className = "android.view.SurfaceControl"; 38 | } else { 39 | className = "android.view.Surface"; 40 | } 41 | 42 | clazz = Class.forName(className); 43 | } catch (ClassNotFoundException e) { 44 | throw new RuntimeException(e); 45 | } 46 | } 47 | 48 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 49 | public static Bitmap screenshot(int w, int h) { 50 | Bitmap bitmap = null; 51 | 52 | try { 53 | Method declaredMethod; 54 | 55 | int sdkInt = Build.VERSION.SDK_INT; 56 | if (sdkInt >= Build.VERSION_CODES.S) { 57 | // create the DisplayCaptureArgs object by DisplayCaptureArgs$Builder.build() 58 | Class argsClass; 59 | Class innerClass; 60 | if (sdkInt >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 61 | argsClass = Class.forName("android.window.ScreenCapture$DisplayCaptureArgs"); 62 | innerClass = Class.forName("android.window.ScreenCapture$DisplayCaptureArgs$Builder"); 63 | } else { 64 | argsClass = Class.forName("android.view.SurfaceControl$DisplayCaptureArgs"); 65 | innerClass = Class.forName("android.view.SurfaceControl$DisplayCaptureArgs$Builder"); 66 | } 67 | 68 | Method setSzMethod = innerClass.getDeclaredMethod("setSize", int.class, int.class); 69 | Method buildMethod = innerClass.getDeclaredMethod("build"); 70 | 71 | Constructor ctor = innerClass.getDeclaredConstructor(IBinder.class); 72 | Object builder = ctor.newInstance(getBuiltInDisplay()); 73 | setSzMethod.invoke(builder, w, h); 74 | Object args = buildMethod.invoke(builder); 75 | 76 | // call hidden method "ScreenshotHardwareBuffer captureDisplay(DisplayCaptureArgs captureArgs)" 77 | Method captureDisplay = clazz.getDeclaredMethod("captureDisplay", argsClass); 78 | Object hdBuffer = captureDisplay.invoke(null, args); 79 | 80 | Class hdBufferClass = hdBuffer.getClass(); 81 | ColorSpace colorSpace = (ColorSpace) hdBufferClass.getDeclaredMethod("getColorSpace").invoke(hdBuffer); 82 | 83 | try (HardwareBuffer hardwareBuffer = 84 | (HardwareBuffer) hdBufferClass.getDeclaredMethod("getHardwareBuffer").invoke(hdBuffer)) { 85 | bitmap = Bitmap.wrapHardwareBuffer(hardwareBuffer, colorSpace); 86 | } catch (Exception e) { 87 | e.printStackTrace(); 88 | } 89 | } else if (sdkInt >= Build.VERSION_CODES.P) { // Pie+ 90 | declaredMethod = 91 | clazz.getDeclaredMethod( 92 | METHOD_SCREENSHOT, 93 | Rect.class, 94 | Integer.TYPE, 95 | Integer.TYPE, 96 | Integer.TYPE); 97 | bitmap = (Bitmap) declaredMethod.invoke(null, new Rect(), w, h, 0); 98 | } else { 99 | declaredMethod = 100 | clazz.getDeclaredMethod(METHOD_SCREENSHOT, Integer.TYPE, Integer.TYPE); 101 | bitmap = (Bitmap) declaredMethod.invoke(null, new Object[]{w, h}); 102 | } 103 | 104 | if (bitmap != null) System.out.println(">>> bmp generated."); 105 | 106 | } catch (NoSuchMethodException e) { 107 | e.printStackTrace(); 108 | } catch (InvocationTargetException e) { 109 | e.printStackTrace(); 110 | } catch (IllegalAccessException | ClassNotFoundException | InstantiationException e) { 111 | e.printStackTrace(); 112 | } 113 | 114 | return bitmap; 115 | } 116 | 117 | private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { 118 | if (getBuiltInDisplayMethod == null) { 119 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 120 | getBuiltInDisplayMethod = clazz.getMethod("getBuiltInDisplay", Integer.TYPE); 121 | } else { // The method signature has been changed in Android Q+ 122 | getBuiltInDisplayMethod = clazz.getMethod("getInternalDisplayToken"); 123 | } 124 | } 125 | return getBuiltInDisplayMethod; 126 | } 127 | 128 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 129 | public static IBinder getBuiltInDisplay() { 130 | 131 | try { 132 | int sdkInt = Build.VERSION.SDK_INT; 133 | if (sdkInt >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { 134 | long[] displayIds = DisplayControl.getPhysicalDisplayIds(); 135 | if (displayIds != null) { 136 | for (long id : displayIds) { 137 | IBinder binder = DisplayControl.getPhysicalDisplayToken(id); 138 | if (binder != null) 139 | return binder; 140 | } 141 | } 142 | 143 | // fall back to the default id 144 | return DisplayControl.getPhysicalDisplayToken(0); 145 | } 146 | 147 | Method method = getGetBuiltInDisplayMethod(); 148 | if (sdkInt < Build.VERSION_CODES.Q) { 149 | // default display 0 150 | return (IBinder) method.invoke(null, 0); 151 | } 152 | 153 | return (IBinder) method.invoke(null); 154 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 155 | System.err.println("Failed to invoke method " + e); 156 | return null; 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /app/src/main/java/com/rayworks/droidcast/TextUtils.kt: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast 2 | 3 | import android.text.Spannable 4 | import android.text.SpannableStringBuilder 5 | import androidx.core.text.bold 6 | import androidx.core.text.toSpannable 7 | import androidx.core.text.underline 8 | 9 | 10 | class TextUtils { 11 | companion object { 12 | fun format(boldStr: String, underline: String): Spannable = 13 | SpannableStringBuilder().bold { append(boldStr) }.underline { append(underline) }.toSpannable() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/rayworks/droidcast/wrapper/DisplayControl.java: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast.wrapper; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | import android.os.IBinder; 6 | 7 | import androidx.annotation.RequiresApi; 8 | 9 | import java.lang.reflect.InvocationTargetException; 10 | import java.lang.reflect.Method; 11 | 12 | /*** 13 | * Code taken and modified from 14 | * scrcpy 15 | */ 16 | @SuppressLint({"BlockedPrivateApi", "PrivateApi"}) 17 | public final class DisplayControl { 18 | 19 | private static final Class CLASS; 20 | 21 | static { 22 | // On Android 14, execute a separate process with a different classpath and LD_PRELOAD to execute the methods 23 | // required to take a screenshot 24 | // https://github.com/Genymobile/scrcpy/pull/4446#issuecomment-1824818046 25 | Class displayControlClass = null; 26 | try { 27 | Class classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory"); 28 | Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod("createClassLoader", String.class, String.class, String.class, 29 | ClassLoader.class, int.class, boolean.class, String.class); 30 | ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null, 31 | ClassLoader.getSystemClassLoader(), 0, true, null); 32 | 33 | displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl"); 34 | 35 | Method loadMethod = Runtime.class.getDeclaredMethod("loadLibrary0", Class.class, String.class); 36 | loadMethod.setAccessible(true); 37 | loadMethod.invoke(Runtime.getRuntime(), displayControlClass, "android_servers"); 38 | } catch (Throwable e) { 39 | System.err.printf("Could not initialize DisplayControl : %s\n", e); 40 | // Do not throw an exception here, the methods will fail when they are called 41 | } 42 | CLASS = displayControlClass; 43 | } 44 | 45 | private static Method getPhysicalDisplayTokenMethod; 46 | private static Method getPhysicalDisplayIdsMethod; 47 | 48 | private DisplayControl() { 49 | // only static methods 50 | } 51 | 52 | private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { 53 | if (getPhysicalDisplayTokenMethod == null) { 54 | getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); 55 | } 56 | return getPhysicalDisplayTokenMethod; 57 | } 58 | 59 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 60 | public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { 61 | try { 62 | Method method = getGetPhysicalDisplayTokenMethod(); 63 | return (IBinder) method.invoke(null, physicalDisplayId); 64 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 65 | System.err.printf("Could not invoke method : %s\n", e); 66 | return null; 67 | } 68 | } 69 | 70 | private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { 71 | if (getPhysicalDisplayIdsMethod == null) { 72 | getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); 73 | } 74 | return getPhysicalDisplayIdsMethod; 75 | } 76 | 77 | @RequiresApi(api = Build.VERSION_CODES.KITKAT) 78 | public static long[] getPhysicalDisplayIds() { 79 | try { 80 | Method method = getGetPhysicalDisplayIdsMethod(); 81 | return (long[]) method.invoke(null); 82 | } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { 83 | System.err.printf("Could not invoke method : %s\n", e); 84 | return null; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | DroidCast 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/rayworks/droidcast/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.rayworks.droidcast; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | 8 | /** 9 | * Example local unit test, which will execute on the development machine (host). 10 | * 11 | * @see Testing documentation 12 | */ 13 | public class ExampleUnitTest { 14 | @Test 15 | public void additionIsCorrect() throws Exception { 16 | assertEquals(4, 2 + 2); 17 | } 18 | } -------------------------------------------------------------------------------- /artifacts.gradle: -------------------------------------------------------------------------------- 1 | // taken from https://github.com/jayway/AndroidGradleExample/blob/master/artifacts.gradle 2 | 3 | android.applicationVariants.all { variant -> 4 | def appName 5 | //Check if an applicationName property is supplied; if not use the name of the parent project. 6 | if (project.hasProperty("applicationName")) { 7 | appName = applicationName 8 | } else { 9 | appName = parent.name 10 | } 11 | 12 | variant.outputs.all { output -> 13 | outputFileName = "${appName}-${output.baseName}-${variant.versionName}.apk" 14 | } 15 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.9.23' 5 | repositories { 6 | mavenCentral() 7 | google() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:8.6.0' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | mavenCentral() 19 | google() 20 | } 21 | } 22 | 23 | task clean(type: Delete) { 24 | delete rootProject.buildDir 25 | } 26 | -------------------------------------------------------------------------------- /cast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/cast.png -------------------------------------------------------------------------------- /cmd_tool/.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode/ 2 | /tests/build/ -------------------------------------------------------------------------------- /cmd_tool/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018 Genymobile 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /cmd_tool/cmd_runner.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "./utils/error_printer.h" 13 | #include "./utils/string_util.h" 14 | 15 | static const char *adb_command; 16 | 17 | static char *apkname; 18 | 19 | static const char *const fwd_cmd[] = {"forward", "tcp:53516", "tcp:53516"}; 20 | 21 | static const char *const un_fwd_cmd[] = {"forward", "--remove", "tcp:53516"}; 22 | 23 | static const char *remote = "/data/local/tmp/"; 24 | 25 | /* methods taken and modified from https://github.com/Genymobile/scrcpy/ */ 26 | 27 | #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) 28 | 29 | #define BUF_SIZE 1024 * 2 30 | 31 | // NB: Give that the target apk/dex file has been pushed into '/data/local/tmp/' on the device, it's 32 | // still possible to get an error about "could not find class 'com.rayworks.droidcast.Main'". so we 33 | // retrieve the apk source path via 'adb shell pm path your-pkg-name'. 34 | static const char *apk_src_path = NULL; 35 | 36 | static jmp_buf env_alarm; 37 | 38 | static void sig_pipe(int id) 39 | { 40 | printf("SIGPIPE caught\n"); 41 | exit(1); 42 | } 43 | 44 | static inline const char *get_adb_command() 45 | { 46 | if (!adb_command) 47 | { 48 | adb_command = getenv("ADB"); 49 | if (!adb_command) 50 | adb_command = "adb"; 51 | } 52 | return adb_command; 53 | } 54 | 55 | pid_t cmd_execute(const char *path, const char *const argv[]) 56 | { 57 | pid_t pid = fork(); 58 | if (pid == -1) 59 | { 60 | perror("fork"); 61 | return -1; 62 | } 63 | if (pid == 0) 64 | { 65 | execvp(path, (char *const *)argv); 66 | perror("exec"); 67 | _exit(1); 68 | } 69 | return pid; 70 | } 71 | 72 | pid_t adb_execute(const char *serial, const char *const adb_cmd[], int len) 73 | { 74 | const char *cmd[len + 4]; 75 | int i; 76 | cmd[0] = get_adb_command(); 77 | if (serial) 78 | { 79 | cmd[1] = "-s"; 80 | cmd[2] = serial; 81 | i = 3; 82 | } 83 | else 84 | { 85 | i = 1; 86 | } 87 | 88 | memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); 89 | cmd[len + i] = NULL; 90 | return cmd_execute(cmd[0], cmd); 91 | } 92 | 93 | int cmd_simple_wait(pid_t pid, int *exit_code) 94 | { 95 | int status; 96 | int code; 97 | if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) 98 | { 99 | // cannot wait, or exited unexpectedly, probably by a signal 100 | code = -1; 101 | } 102 | else 103 | { 104 | code = WEXITSTATUS(status); 105 | } 106 | if (exit_code) 107 | { 108 | *exit_code = code; 109 | } 110 | return !code; 111 | } 112 | 113 | void wait_for_child_process(pid_t proc, char *p_cmd) 114 | { 115 | int exit_code; 116 | if (!cmd_simple_wait(proc, &exit_code)) 117 | { 118 | if (exit_code != -1) 119 | { 120 | printf("Cmd \'%s\' : return value %d\n", p_cmd, exit_code); 121 | } 122 | else 123 | { 124 | printf("Cmd \'%s\' : exited unexpectedly\n", p_cmd); 125 | } 126 | 127 | perror(p_cmd); 128 | exit(-1); 129 | } 130 | else 131 | { 132 | printf("Cmd \'%s\' executed successfully\n", p_cmd); 133 | } 134 | } 135 | 136 | static void handler(int sig) 137 | { 138 | printf("\n> signal caught : %d\n", sig); 139 | 140 | int count = 0; 141 | 142 | /* 143 | UNSAFE: Non-async-signal-safe functions used. 144 | */ 145 | if (sig == SIGCHLD) 146 | { 147 | // reset 148 | signal(SIGCHLD, SIG_DFL); 149 | 150 | int status; 151 | pid_t child_proc; 152 | if ((child_proc = waitpid(-1, &status, 0)) > 0) 153 | { 154 | printf("handler : Reaped child %ld\n", (long)child_proc); 155 | if (WIFEXITED(status)) 156 | { 157 | printf("child exited, with status : %d\n", WEXITSTATUS(status)); 158 | } 159 | else if (WIFSIGNALED(status)) 160 | { // not reached ?! 161 | printf("child killed by signal %d (%s)\n", WTERMSIG(status), strsignal(WTERMSIG(status))); 162 | } 163 | } 164 | else 165 | { 166 | 167 | perror("waitpid error"); 168 | exit(EXIT_FAILURE); 169 | } 170 | 171 | count++; 172 | printf("Caught SIGCHLD : %d time(s) \n", count); 173 | 174 | pid_t proc = adb_execute(NULL, un_fwd_cmd, ARRAY_LEN(un_fwd_cmd)); 175 | wait_for_child_process(proc, "adb undo forward"); 176 | } 177 | else if (sig == SIGALRM) 178 | { 179 | signal(SIGALRM, SIG_DFL); 180 | 181 | system("open http://localhost:53516/screenshot"); 182 | 183 | longjmp(env_alarm, 1); 184 | } 185 | } 186 | 187 | // Use a pipe to communicate between a parent and child process (via Android Package Manager) for 188 | // retrieving the actual installed apk path. 189 | static int resolve_apk_path() 190 | { 191 | char cmd[BUF_SIZE]; 192 | const char *cmd_fmt = "%s shell pm path com.rayworks.droidcast"; 193 | snprintf(cmd, BUF_SIZE, cmd_fmt, get_adb_command()); 194 | 195 | FILE *pfin; 196 | if ((pfin = popen(cmd, "r")) == NULL) 197 | err_sys("popen error"); 198 | 199 | int fd = fileno(pfin); 200 | 201 | char buffer[BUF_SIZE]; 202 | char result[BUF_SIZE]; 203 | memset(result, 0, BUF_SIZE); 204 | 205 | int readCnt = 0; 206 | while (1) 207 | { 208 | ssize_t count = read(fd, buffer, sizeof(buffer)); 209 | printf("bytes %ld read from pipe\n", count); 210 | 211 | if (count == -1) 212 | { 213 | if (errno == EINTR) 214 | { 215 | continue; 216 | } 217 | else 218 | { 219 | perror("read"); 220 | exit(1); 221 | } 222 | } 223 | else if (count == 0) 224 | { 225 | break; 226 | } 227 | else 228 | { 229 | // NB: the read content could be discontinued, so gather all the content first 230 | char *ptr = result; 231 | 232 | strncpy(ptr + readCnt, buffer, count); 233 | readCnt += count; 234 | } 235 | } 236 | 237 | int ret = pclose(pfin); 238 | if (ret == -1) 239 | { 240 | err_sys("pclose err"); 241 | } 242 | else if (ret == 127) 243 | { 244 | fprintf(stderr, "bad command : %s\n", cmd); 245 | exit(1); 246 | } 247 | else if (ret != 0) 248 | { 249 | printf("exit status : %d\n", ret); 250 | if (WIFEXITED(ret)) 251 | printf("Normal termination, exit status = %d\n", WEXITSTATUS(ret)); 252 | else if (WIFSIGNALED(ret)) 253 | { 254 | printf("abnoraml termination, signal number : %d%s\n", WTERMSIG(ret), 255 | #ifdef WCOREDUMP 256 | WCOREDUMP(ret) ? "Core file generated" : "" 257 | #else 258 | ""); 259 | #endif 260 | ); 261 | } 262 | 263 | err_msg("Fatal error: Apk path can't be retrieved, Have you installed the app successfully?"); 264 | exit(-1); 265 | } 266 | 267 | if (readCnt > 0) 268 | { 269 | // format like: 270 | // package:/data/app/com.rayworks.droidcast-Tb1-e8DHFvuQ1wI6_MlLww==/base.apk 271 | apk_src_path = filter_apk_path(result); 272 | if (!apk_src_path) 273 | { 274 | err_msg("Fatal error: Apk path can't be retrieved, Have you installed the app successfully?"); 275 | exit(-1); 276 | } 277 | 278 | printf("Target path is : %s\n", apk_src_path); 279 | } 280 | 281 | return readCnt; 282 | } 283 | 284 | void sleep_ext(unsigned int seconds, void (*func)(int)) 285 | { 286 | if (signal(SIGALRM, func) == SIG_ERR) 287 | { 288 | perror("error : signal SIGALRM"); 289 | exit(EXIT_FAILURE); 290 | } 291 | 292 | if (setjmp(env_alarm) == 0) 293 | { 294 | alarm(seconds); 295 | pause(); /* next caught signal will wake this up */ 296 | } 297 | } 298 | 299 | int main(int argc, char *argv[]) 300 | { 301 | if (resolve_apk_path() == 0) 302 | { 303 | perror("Apk not found, exit now\n"); 304 | exit(-1); 305 | }; 306 | 307 | pid_t proc = adb_execute(NULL, fwd_cmd, ARRAY_LEN(fwd_cmd)); 308 | wait_for_child_process(proc, "adb forward"); 309 | 310 | char class_path[256]; 311 | if (apk_src_path) 312 | { 313 | snprintf(class_path, sizeof(class_path), "CLASSPATH=%s", apk_src_path); 314 | } 315 | else 316 | { 317 | snprintf(class_path, sizeof(class_path), "CLASSPATH=%s%s", remote, apkname); 318 | } 319 | printf("> full class path: %s\n", class_path); 320 | 321 | // setup the handler for monitoring the core child process quitting 322 | if (signal(SIGCHLD, handler) == SIG_ERR) 323 | { 324 | perror("error: signal SIGCHLD"); 325 | exit(EXIT_FAILURE); 326 | } 327 | if (signal(SIGINT, handler) == SIG_ERR) 328 | { 329 | // SIGINT will kill the child process which already ran with 'execvp' (the handler 330 | // function won't be inherited anymore); for parent process, just ignore this signal 331 | // and wait for the quitting of the child. 332 | perror("error: signal SIGINT"); 333 | exit(EXIT_FAILURE); 334 | } 335 | 336 | const char *const cmd[] = { 337 | "shell", 338 | class_path, 339 | "app_process", 340 | "/", // unused 341 | "com.rayworks.droidcast.Main"}; 342 | 343 | adb_execute(NULL, cmd, ARRAY_LEN(cmd)); 344 | 345 | // delay opening the default browser to make sure the server is ready 346 | sleep_ext(2, handler); 347 | 348 | int status; 349 | pid_t childPid; 350 | while ((childPid = waitpid(-1, &status, 0)) > 0) 351 | { 352 | continue; 353 | } 354 | 355 | if (childPid == -1) 356 | { 357 | if (errno == ECHILD) 358 | { 359 | printf("No more child process to be waiting, main process exiting now.\n"); 360 | } 361 | else 362 | { 363 | perror("waitpid"); 364 | exit(EXIT_FAILURE); 365 | } 366 | } 367 | 368 | exit(EXIT_SUCCESS); 369 | } -------------------------------------------------------------------------------- /cmd_tool/makefile: -------------------------------------------------------------------------------- 1 | cmd_tool: cmd_runner.c utils/error_printer.h utils/error_printer.c utils/string_util.h utils/string_util.c 2 | gcc -o prog cmd_runner.c utils/error_printer.c utils/string_util.c -I. 3 | clean: 4 | rm ./prog -------------------------------------------------------------------------------- /cmd_tool/tests/README.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | This is the unit tests fold for cmd_tool. Run the follow commands to start testing: 4 | 5 | `meson build` 6 | 7 | `cd ./build` 8 | 9 | `meson test` 10 | 11 | See more about [`Unit tests` with `Meson`](http://mesonbuild.com/Unit-tests.html) 12 | 13 | ## Dependencies 14 | 15 | * [Meson](https://mesonbuild.com/Getting-meson.html) 16 | * [Ninja](https://ninja-build.org) (version 1.5 or newer) 17 | -------------------------------------------------------------------------------- /cmd_tool/tests/meson.build: -------------------------------------------------------------------------------- 1 | project('cmd_tool', 'c') 2 | 3 | src = ['../utils/string_util.c', 'util_test.c'] 4 | 5 | e = executable('cmd_tool', src) 6 | test('test filtering apk path', e) -------------------------------------------------------------------------------- /cmd_tool/tests/util_test.c: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Sean Zhou on 11/13/18. 3 | // 4 | 5 | #include 6 | #include 7 | 8 | #include "../utils/string_util.h" 9 | 10 | static void test_filtering(void) { 11 | char* result = "package:/data/app/com.rayworks.droidcast-Tb1-e8DHFvuQ1wI6_MlLww==/base.apk"; 12 | char * pstr = filter_apk_path(result); 13 | assert(!strcmp(pstr, "/data/app/com.rayworks.droidcast-Tb1-e8DHFvuQ1wI6_MlLww==/base.apk")); 14 | } 15 | 16 | static void test_filtering_kitkat(void) { 17 | char* result = "package:/data/app/com.rayworks.droidcast-2.apk"; 18 | char * pstr = filter_apk_path(result); 19 | assert(!strcmp(pstr, "/data/app/com.rayworks.droidcast-2.apk")); 20 | } 21 | 22 | int main(int argc, char const *argv[]) 23 | { 24 | test_filtering(); 25 | test_filtering_kitkat(); 26 | 27 | return 0; 28 | } 29 | -------------------------------------------------------------------------------- /cmd_tool/utils/error_printer.c: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Sean Zhou on 9/5/18. 3 | // 4 | 5 | #include "error_printer.h" 6 | 7 | char* strerror(int error) { 8 | static char mesg[30]; 9 | 10 | // if (error >= 0 && error <= sys_nerr) 11 | // return ((char *) sys_errlist[error]); 12 | 13 | sprintf(mesg, "Unknown error (%d)", error); 14 | return (mesg); 15 | } 16 | 17 | /* 18 | * Print a message and return to caller. 19 | * Caller specifies "errnoflag". 20 | */ 21 | static void err_doit(int errnoflag, int error, const char *fmt, va_list ap) { 22 | char buf[MAXLINE]; 23 | 24 | vsnprintf(buf, MAXLINE - 1, fmt, ap); 25 | if (errnoflag) 26 | snprintf(buf + strlen(buf), MAXLINE - strlen(buf) - 1, ": %s", 27 | strerror(error)); 28 | strcat(buf, "\n"); 29 | fflush(stdout); /* in case stdout and stderr are the same */ 30 | fputs(buf, stderr); 31 | fflush(NULL); /* flushes all stdio output streams */ 32 | } 33 | 34 | /* 35 | * Nonfatal error unrelated to a system call. 36 | * Print a message and return. 37 | */ 38 | void err_msg(const char *fmt, ...) { 39 | va_list ap; 40 | 41 | va_start(ap, fmt); 42 | err_doit(0, 0, fmt, ap); 43 | va_end(ap); 44 | } 45 | 46 | /* 47 | * Fatal error related to a system call. 48 | * Print a message and terminate. 49 | */ 50 | void err_sys(const char *fmt, ...) { 51 | va_list ap; 52 | 53 | va_start(ap, fmt); 54 | err_doit(1, errno, fmt, ap); 55 | va_end(ap); 56 | exit(1); 57 | } 58 | -------------------------------------------------------------------------------- /cmd_tool/utils/error_printer.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Sean Zhou on 9/5/18. 3 | // 4 | 5 | #ifndef DROIDCAST_ERROR_PRINTER_H 6 | #define DROIDCAST_ERROR_PRINTER_H 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include /* for definition of errno */ 15 | #include /* ISO C variable aruments */ 16 | #include 17 | #include 18 | 19 | #define MAXLINE 1024 20 | 21 | char* strerror(int error); 22 | static void err_doit(int errnoflag, int error, const char *fmt, va_list ap); 23 | void err_msg(const char *fmt, ...); 24 | void err_sys(const char *fmt, ...); 25 | 26 | #endif //DROIDCAST_ERROR_PRINTER_H 27 | -------------------------------------------------------------------------------- /cmd_tool/utils/string_util.c: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Sean Zhou on 11/13/18. 3 | // 4 | 5 | #include "string_util.h" 6 | 7 | char* filter_apk_path(char* result) { 8 | // format like: 9 | // package:/data/app/com.rayworks.droidcast-Tb1-e8DHFvuQ1wI6_MlLww==/base.apk 10 | 11 | // on OS 4.1 12 | // package:/data/app/com.rayworks.droidcast-1.apk 13 | 14 | char* pstart = strchr(result, ':'); 15 | if(!pstart) 16 | return NULL; 17 | 18 | pstart++; 19 | 20 | const char* pBaseApk = ".apk"; 21 | char* pend = strstr(result, pBaseApk); 22 | pend += strlen(pBaseApk) - 1; 23 | 24 | printf("filter string : %s", result); 25 | 26 | char* pstr = (char*) malloc((pend - pstart + 2) * sizeof (char)); 27 | pstr[pend - pstart + 1] = 0; // terminator added 28 | memcpy(pstr, pstart, pend - pstart + 1); 29 | 30 | return pstr; 31 | } 32 | -------------------------------------------------------------------------------- /cmd_tool/utils/string_util.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Sean Zhou on 11/13/18. 3 | // 4 | 5 | #ifndef DROIDCAST_STRING_UTIL_H 6 | #define DROIDCAST_STRING_UTIL_H 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #endif //DROIDCAST_STRING_UTIL_H 13 | 14 | char* filter_apk_path(char* result); -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.defaults.buildfeatures.buildconfig=true 10 | android.enableJetifier=true 11 | android.nonFinalResIds=false 12 | android.nonTransitiveRClass=false 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /images/payment_cn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/images/payment_cn.jpg -------------------------------------------------------------------------------- /process_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/process_main.png -------------------------------------------------------------------------------- /screen_shot_dock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayworks/DroidCast/5c9dc5e9be82533a5e109a7a2204922e9121d64b/screen_shot_dock.png -------------------------------------------------------------------------------- /script-rs/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea/ -------------------------------------------------------------------------------- /script-rs/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.6.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 10 | 11 | [[package]] 12 | name = "block2" 13 | version = "0.5.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" 16 | dependencies = [ 17 | "objc2", 18 | ] 19 | 20 | [[package]] 21 | name = "bumpalo" 22 | version = "3.12.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 25 | 26 | [[package]] 27 | name = "bytes" 28 | version = "1.2.1" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 31 | 32 | [[package]] 33 | name = "cesu8" 34 | version = "1.1.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 37 | 38 | [[package]] 39 | name = "cfg-if" 40 | version = "1.0.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 43 | 44 | [[package]] 45 | name = "combine" 46 | version = "4.6.6" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" 49 | dependencies = [ 50 | "bytes", 51 | "memchr", 52 | ] 53 | 54 | [[package]] 55 | name = "core-foundation" 56 | version = "0.10.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" 59 | dependencies = [ 60 | "core-foundation-sys", 61 | "libc", 62 | ] 63 | 64 | [[package]] 65 | name = "core-foundation-sys" 66 | version = "0.8.7" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 69 | 70 | [[package]] 71 | name = "form_urlencoded" 72 | version = "1.0.1" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" 75 | dependencies = [ 76 | "matches", 77 | "percent-encoding", 78 | ] 79 | 80 | [[package]] 81 | name = "home" 82 | version = "0.5.9" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 85 | dependencies = [ 86 | "windows-sys 0.52.0", 87 | ] 88 | 89 | [[package]] 90 | name = "idna" 91 | version = "0.2.3" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" 94 | dependencies = [ 95 | "matches", 96 | "unicode-bidi", 97 | "unicode-normalization", 98 | ] 99 | 100 | [[package]] 101 | name = "jni" 102 | version = "0.21.1" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" 105 | dependencies = [ 106 | "cesu8", 107 | "cfg-if", 108 | "combine", 109 | "jni-sys", 110 | "log", 111 | "thiserror", 112 | "walkdir", 113 | "windows-sys 0.45.0", 114 | ] 115 | 116 | [[package]] 117 | name = "jni-sys" 118 | version = "0.3.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 121 | 122 | [[package]] 123 | name = "js-sys" 124 | version = "0.3.59" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" 127 | dependencies = [ 128 | "wasm-bindgen", 129 | ] 130 | 131 | [[package]] 132 | name = "libc" 133 | version = "0.2.132" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" 136 | 137 | [[package]] 138 | name = "log" 139 | version = "0.4.17" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 142 | dependencies = [ 143 | "cfg-if", 144 | ] 145 | 146 | [[package]] 147 | name = "matches" 148 | version = "0.1.9" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 151 | 152 | [[package]] 153 | name = "memchr" 154 | version = "2.5.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 157 | 158 | [[package]] 159 | name = "ndk-context" 160 | version = "0.1.1" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" 163 | 164 | [[package]] 165 | name = "objc-sys" 166 | version = "0.3.5" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" 169 | 170 | [[package]] 171 | name = "objc2" 172 | version = "0.5.2" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" 175 | dependencies = [ 176 | "objc-sys", 177 | "objc2-encode", 178 | ] 179 | 180 | [[package]] 181 | name = "objc2-encode" 182 | version = "4.0.3" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" 185 | 186 | [[package]] 187 | name = "objc2-foundation" 188 | version = "0.2.2" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" 191 | dependencies = [ 192 | "bitflags", 193 | "block2", 194 | "libc", 195 | "objc2", 196 | ] 197 | 198 | [[package]] 199 | name = "once_cell" 200 | version = "1.14.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" 203 | 204 | [[package]] 205 | name = "percent-encoding" 206 | version = "2.1.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 209 | 210 | [[package]] 211 | name = "proc-macro2" 212 | version = "1.0.92" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 215 | dependencies = [ 216 | "unicode-ident", 217 | ] 218 | 219 | [[package]] 220 | name = "quote" 221 | version = "1.0.38" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 224 | dependencies = [ 225 | "proc-macro2", 226 | ] 227 | 228 | [[package]] 229 | name = "same-file" 230 | version = "1.0.6" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 233 | dependencies = [ 234 | "winapi-util", 235 | ] 236 | 237 | [[package]] 238 | name = "script-rs" 239 | version = "0.1.0" 240 | dependencies = [ 241 | "signal-hook", 242 | "webbrowser", 243 | ] 244 | 245 | [[package]] 246 | name = "signal-hook" 247 | version = "0.3.17" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 250 | dependencies = [ 251 | "libc", 252 | "signal-hook-registry", 253 | ] 254 | 255 | [[package]] 256 | name = "signal-hook-registry" 257 | version = "1.4.0" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 260 | dependencies = [ 261 | "libc", 262 | ] 263 | 264 | [[package]] 265 | name = "syn" 266 | version = "1.0.99" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" 269 | dependencies = [ 270 | "proc-macro2", 271 | "quote", 272 | "unicode-ident", 273 | ] 274 | 275 | [[package]] 276 | name = "thiserror" 277 | version = "1.0.34" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" 280 | dependencies = [ 281 | "thiserror-impl", 282 | ] 283 | 284 | [[package]] 285 | name = "thiserror-impl" 286 | version = "1.0.34" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" 289 | dependencies = [ 290 | "proc-macro2", 291 | "quote", 292 | "syn", 293 | ] 294 | 295 | [[package]] 296 | name = "tinyvec" 297 | version = "1.6.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 300 | dependencies = [ 301 | "tinyvec_macros", 302 | ] 303 | 304 | [[package]] 305 | name = "tinyvec_macros" 306 | version = "0.1.0" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 309 | 310 | [[package]] 311 | name = "unicode-bidi" 312 | version = "0.3.8" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" 315 | 316 | [[package]] 317 | name = "unicode-ident" 318 | version = "1.0.3" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" 321 | 322 | [[package]] 323 | name = "unicode-normalization" 324 | version = "0.1.21" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" 327 | dependencies = [ 328 | "tinyvec", 329 | ] 330 | 331 | [[package]] 332 | name = "url" 333 | version = "2.2.2" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" 336 | dependencies = [ 337 | "form_urlencoded", 338 | "idna", 339 | "matches", 340 | "percent-encoding", 341 | ] 342 | 343 | [[package]] 344 | name = "walkdir" 345 | version = "2.3.2" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" 348 | dependencies = [ 349 | "same-file", 350 | "winapi", 351 | "winapi-util", 352 | ] 353 | 354 | [[package]] 355 | name = "wasm-bindgen" 356 | version = "0.2.82" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" 359 | dependencies = [ 360 | "cfg-if", 361 | "wasm-bindgen-macro", 362 | ] 363 | 364 | [[package]] 365 | name = "wasm-bindgen-backend" 366 | version = "0.2.82" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" 369 | dependencies = [ 370 | "bumpalo", 371 | "log", 372 | "once_cell", 373 | "proc-macro2", 374 | "quote", 375 | "syn", 376 | "wasm-bindgen-shared", 377 | ] 378 | 379 | [[package]] 380 | name = "wasm-bindgen-macro" 381 | version = "0.2.82" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" 384 | dependencies = [ 385 | "quote", 386 | "wasm-bindgen-macro-support", 387 | ] 388 | 389 | [[package]] 390 | name = "wasm-bindgen-macro-support" 391 | version = "0.2.82" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" 394 | dependencies = [ 395 | "proc-macro2", 396 | "quote", 397 | "syn", 398 | "wasm-bindgen-backend", 399 | "wasm-bindgen-shared", 400 | ] 401 | 402 | [[package]] 403 | name = "wasm-bindgen-shared" 404 | version = "0.2.82" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" 407 | 408 | [[package]] 409 | name = "web-sys" 410 | version = "0.3.59" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" 413 | dependencies = [ 414 | "js-sys", 415 | "wasm-bindgen", 416 | ] 417 | 418 | [[package]] 419 | name = "webbrowser" 420 | version = "1.0.3" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535" 423 | dependencies = [ 424 | "block2", 425 | "core-foundation", 426 | "home", 427 | "jni", 428 | "log", 429 | "ndk-context", 430 | "objc2", 431 | "objc2-foundation", 432 | "url", 433 | "web-sys", 434 | ] 435 | 436 | [[package]] 437 | name = "winapi" 438 | version = "0.3.9" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 441 | dependencies = [ 442 | "winapi-i686-pc-windows-gnu", 443 | "winapi-x86_64-pc-windows-gnu", 444 | ] 445 | 446 | [[package]] 447 | name = "winapi-i686-pc-windows-gnu" 448 | version = "0.4.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 451 | 452 | [[package]] 453 | name = "winapi-util" 454 | version = "0.1.5" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 457 | dependencies = [ 458 | "winapi", 459 | ] 460 | 461 | [[package]] 462 | name = "winapi-x86_64-pc-windows-gnu" 463 | version = "0.4.0" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 466 | 467 | [[package]] 468 | name = "windows-sys" 469 | version = "0.45.0" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 472 | dependencies = [ 473 | "windows-targets 0.42.2", 474 | ] 475 | 476 | [[package]] 477 | name = "windows-sys" 478 | version = "0.52.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 481 | dependencies = [ 482 | "windows-targets 0.52.6", 483 | ] 484 | 485 | [[package]] 486 | name = "windows-targets" 487 | version = "0.42.2" 488 | source = "registry+https://github.com/rust-lang/crates.io-index" 489 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 490 | dependencies = [ 491 | "windows_aarch64_gnullvm 0.42.2", 492 | "windows_aarch64_msvc 0.42.2", 493 | "windows_i686_gnu 0.42.2", 494 | "windows_i686_msvc 0.42.2", 495 | "windows_x86_64_gnu 0.42.2", 496 | "windows_x86_64_gnullvm 0.42.2", 497 | "windows_x86_64_msvc 0.42.2", 498 | ] 499 | 500 | [[package]] 501 | name = "windows-targets" 502 | version = "0.52.6" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 505 | dependencies = [ 506 | "windows_aarch64_gnullvm 0.52.6", 507 | "windows_aarch64_msvc 0.52.6", 508 | "windows_i686_gnu 0.52.6", 509 | "windows_i686_gnullvm", 510 | "windows_i686_msvc 0.52.6", 511 | "windows_x86_64_gnu 0.52.6", 512 | "windows_x86_64_gnullvm 0.52.6", 513 | "windows_x86_64_msvc 0.52.6", 514 | ] 515 | 516 | [[package]] 517 | name = "windows_aarch64_gnullvm" 518 | version = "0.42.2" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 521 | 522 | [[package]] 523 | name = "windows_aarch64_gnullvm" 524 | version = "0.52.6" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 527 | 528 | [[package]] 529 | name = "windows_aarch64_msvc" 530 | version = "0.42.2" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 533 | 534 | [[package]] 535 | name = "windows_aarch64_msvc" 536 | version = "0.52.6" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 539 | 540 | [[package]] 541 | name = "windows_i686_gnu" 542 | version = "0.42.2" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 545 | 546 | [[package]] 547 | name = "windows_i686_gnu" 548 | version = "0.52.6" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 551 | 552 | [[package]] 553 | name = "windows_i686_gnullvm" 554 | version = "0.52.6" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 557 | 558 | [[package]] 559 | name = "windows_i686_msvc" 560 | version = "0.42.2" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 563 | 564 | [[package]] 565 | name = "windows_i686_msvc" 566 | version = "0.52.6" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 569 | 570 | [[package]] 571 | name = "windows_x86_64_gnu" 572 | version = "0.42.2" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 575 | 576 | [[package]] 577 | name = "windows_x86_64_gnu" 578 | version = "0.52.6" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 581 | 582 | [[package]] 583 | name = "windows_x86_64_gnullvm" 584 | version = "0.42.2" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 587 | 588 | [[package]] 589 | name = "windows_x86_64_gnullvm" 590 | version = "0.52.6" 591 | source = "registry+https://github.com/rust-lang/crates.io-index" 592 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 593 | 594 | [[package]] 595 | name = "windows_x86_64_msvc" 596 | version = "0.42.2" 597 | source = "registry+https://github.com/rust-lang/crates.io-index" 598 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 599 | 600 | [[package]] 601 | name = "windows_x86_64_msvc" 602 | version = "0.52.6" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 605 | -------------------------------------------------------------------------------- /script-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "script-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | webbrowser = "1.0.3" 10 | 11 | signal-hook = "0.3.17" -------------------------------------------------------------------------------- /script-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process::Command; 3 | 4 | use std::{error::Error, thread}; 5 | use std::time::Duration; 6 | use signal_hook::{iterator::Signals, consts::SIGINT}; 7 | 8 | use webbrowser; 9 | 10 | fn main() { 11 | setup_signal_handler().expect("Failed to set up signal handler"); 12 | 13 | let args: Vec = env::args().collect(); 14 | println!(">>> args {:?}", args); 15 | 16 | if args.len() != 2 && args.len() != 3 { 17 | println!("usage : prog port [serial number]"); 18 | return; 19 | } 20 | 21 | let port = args[1].to_string(); 22 | port.trim().parse::().expect("The port must be a number."); 23 | 24 | // adb devices 25 | let dev_cnt = count_connected_devices(); 26 | if dev_cnt < 2 { 27 | println!("Make sure your device is connected"); 28 | return; 29 | } else if dev_cnt > 2 { 30 | if args.len() < 3 { 31 | println!("Multiple devices connected, please specify the target device serial number"); 32 | return; 33 | } 34 | } 35 | 36 | // apk path 37 | let full_path = locate_apk_path(); 38 | if full_path.is_empty() { 39 | return; 40 | } 41 | 42 | // forward 43 | forward_connection(&port); 44 | 45 | let handle = thread::spawn(|| { 46 | thread::sleep(Duration::from_secs(2)); 47 | 48 | println!("Open the browser on the worker thread"); 49 | open_browser(); 50 | }); 51 | 52 | startup_service_and_wait(&port, full_path); 53 | 54 | // unforward 55 | unforward_connection(&port); 56 | 57 | handle.join().unwrap(); 58 | println!("About to quit the app"); 59 | } 60 | 61 | fn serial_checked_command() -> Command { 62 | let mut cmd = Command::new("adb"); 63 | let args: Vec = env::args().collect(); 64 | if args.len() >= 3 { 65 | cmd.args(vec!["-s", &args[2]]); 66 | } 67 | cmd 68 | } 69 | 70 | fn count_connected_devices() -> usize { 71 | let devices_result = Command::new("adb").arg("devices").output(); 72 | let s = devices_result.unwrap().stdout; 73 | let devices_out = std::str::from_utf8(&s).unwrap(); 74 | 75 | println!("{}", format!("\nDevices info : {}", devices_out)); 76 | devices_out.matches("device").count() 77 | } 78 | 79 | fn locate_apk_path() -> String { 80 | let params = vec!["shell", "pm", "path", "com.rayworks.droidcast"]; 81 | let path_result = serial_checked_command().args(params).output(); 82 | let path = path_result.unwrap().stdout; 83 | 84 | let mut raw_path = std::str::from_utf8(&path).unwrap(); 85 | raw_path = raw_path.split(":").last().unwrap().trim(); 86 | if raw_path.is_empty() { 87 | eprintln!("Apk not found, have you installed it successfully?"); 88 | return "".to_string(); 89 | } 90 | let full_path = String::from("CLASSPATH=") + &(raw_path.to_string()); 91 | println!("Path {}", full_path); 92 | 93 | full_path 94 | } 95 | 96 | fn startup_service_and_wait(port: &String, full_path: String) { 97 | // app_process 98 | let port_param = String::from("--port=") + &port; 99 | let params = vec![ 100 | "shell", 101 | full_path.trim(), 102 | "app_process", 103 | "/", 104 | "com.rayworks.droidcast.Main", 105 | port_param.as_str().trim(), 106 | ]; 107 | println!("Params -> {:?}", params); 108 | 109 | let result = serial_checked_command().args(params) 110 | .spawn() 111 | .unwrap() 112 | .wait_with_output() 113 | .expect("Failed to wait for a Child Process"); 114 | 115 | println!("status: {}", result.status); 116 | } 117 | 118 | fn forward_connection(port: &String) { 119 | let grp = String::from("tcp:") + &port; 120 | let params_fwd = vec!["forward", &grp.trim(), &grp.trim()]; 121 | println!("Params -> {:?}", params_fwd); 122 | 123 | serial_checked_command().args(params_fwd).output().expect("Failed to forward the tcp connection"); 124 | } 125 | 126 | fn unforward_connection(port: &String) { 127 | let tcp = format!("tcp:{}", &port); 128 | let params_fwd = vec!["forward", "--remove", &tcp]; 129 | 130 | let status = serial_checked_command().args(params_fwd).output().unwrap().status; 131 | println!("adb unforward action status : {:?}", status); 132 | } 133 | 134 | fn setup_signal_handler() -> Result<(), Box> { 135 | let mut signals = Signals::new(&[SIGINT])?; 136 | 137 | thread::spawn(move || { 138 | for sig in signals.forever() { 139 | println!("\nReceived signal {:?}", sig); 140 | } 141 | }); 142 | 143 | Ok(()) 144 | } 145 | 146 | fn open_browser() { 147 | let args: Vec = env::args().collect(); 148 | 149 | let ip_param = vec!["shell", "ip route | awk '/wlan*/{ print $9 }'| tr -d '\n'"]; 150 | let ip = serial_checked_command().args(ip_param).output().unwrap().stdout; 151 | let ip = std::str::from_utf8(&ip).unwrap().to_string(); 152 | println!(">>> Share the url 'http://{}:{}/screenshot' to see the live screen", ip, args[1]); 153 | 154 | let url = format!("http://localhost:{}/screenshot", args[1]); 155 | if webbrowser::open(&url).is_err() { 156 | println!("Failed to open browser"); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | The python script 'automation.py' written in Python 2.7.15 which is not 4 | compatible with version 3.x. If you have Python 3.6+ installed, there 5 | are a few steps to do code translation : 6 | 7 | * transform it into valid Python 3.x code by official library [2to3](https://docs.python.org/2/library/2to3.html) 8 | 9 | ```python 10 | 2to3 -w automation.py 11 | ``` 12 | 13 | * update the code at L35-L36 to : 14 | ```python 15 | p = subprocess.Popen([str(arg) for arg in args], stdout=out, encoding='utf-8') 16 | ``` -------------------------------------------------------------------------------- /scripts/auto_connector.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python -u 2 | 3 | # Script (generated for Python 3.6+) to automate the configurations to show the screenshot on your 4 | # default web browser. 5 | # To get started, simply run : 'python ./automation.py' 6 | 7 | import subprocess 8 | import webbrowser 9 | import argparse 10 | import signal 11 | 12 | from threading import Timer 13 | 14 | adb = ['adb'] 15 | 16 | parser = argparse.ArgumentParser( 17 | description='Automation script to connect the current Android device wireless') 18 | parser.add_argument('-s', '--serial', dest='device_serial', 19 | help='Device serial number (adb -s option)') 20 | parser.add_argument( 21 | '-p', 22 | '--port', 23 | dest='port', 24 | nargs='?', 25 | const=5555, 26 | type=int, 27 | default=5555, 28 | help='Port number to be listening on via TCP, by default it\'s 5555') 29 | args_in = parser.parse_args() 30 | 31 | 32 | def run_adb(args, pipeOutput=True): 33 | if (args_in.device_serial): 34 | args = adb + ['-s', args_in.device_serial] + args 35 | else: 36 | args = adb + args 37 | 38 | # print('exec cmd : %s' % args) 39 | out = None 40 | if (pipeOutput): 41 | out = subprocess.PIPE 42 | 43 | p = subprocess.Popen([str(arg) 44 | for arg in args], stdout=out, encoding='utf-8') 45 | stdout, stderr = p.communicate() 46 | return (p.returncode, stdout, stderr) 47 | 48 | 49 | def identify_device(): 50 | (rc, out, _) = run_adb(["devices"]) 51 | if (rc): 52 | raise RuntimeError("Fail to find devices") 53 | else: 54 | # Output as following: 55 | # List of devices attached 56 | # 6466eb0c device 57 | print(out) 58 | device_serial_no = args_in.device_serial 59 | 60 | devicesInfo = str(out) 61 | deviceCnt = devicesInfo.count('device') - 1 62 | 63 | if deviceCnt < 1: 64 | raise RuntimeError("Fail to find devices") 65 | 66 | if (deviceCnt > 1 and (not device_serial_no)): 67 | raise RuntimeError( 68 | "Please specify the serial number of target device you want to use ('-s serial_number').") 69 | 70 | 71 | def start_tcp_ip(ip): 72 | print(">>> listening on TCP on %d" % args_in.port) 73 | run_adb(['tcpip', args_in.port]) 74 | 75 | print(">>> adb connect") 76 | (rc, out, _) = run_adb(['connect', ip]) 77 | if rc == 0: 78 | print("Device connected from %s" % ip) 79 | 80 | 81 | def retrieve_ip(): 82 | # ip route: 83 | # e.g. 192.168.0.0/24 dev wlan0 proto kernel scope link src 192.168.0.125 84 | (rc, out, _) = run_adb( 85 | ["shell", "ip route | awk '/wlan*/{ print $9 }'| tr -d '\n'"]) 86 | print("device ip : %s" % out) 87 | return out 88 | 89 | 90 | def handler(signum, frame): 91 | print('\n>>> Signal caught: ', signum) 92 | if ip: 93 | (_, out, err) = run_adb(['disconnect', ip]) 94 | print(">>> Device disconnected from %d" % ip) 95 | 96 | 97 | ip = "" 98 | 99 | 100 | def automate(): 101 | # handle the keyboard interruption explicitly 102 | signal.signal(signal.SIGINT, handler) 103 | 104 | try: 105 | identify_device() 106 | 107 | ip = retrieve_ip() 108 | start_tcp_ip(ip) 109 | 110 | except Exception as e: 111 | print(e) 112 | 113 | 114 | if __name__ == "__main__": 115 | automate() 116 | -------------------------------------------------------------------------------- /scripts/automation.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python -u 2 | 3 | # Script (written in Python 2.7.15) to automate the configurations to show the screenshot on your 4 | # default web browser. 5 | # To get started, simply run : 'python ./automation.py' 6 | 7 | import subprocess 8 | import webbrowser 9 | import argparse 10 | 11 | from threading import Timer 12 | 13 | adb = ['adb'] 14 | 15 | parser = argparse.ArgumentParser( 16 | description='Automation script to activate capturing screenshot of Android device') 17 | parser.add_argument('-s', '--serial', dest='device_serial', 18 | help='Device serial number (adb -s option)') 19 | parser.add_argument( 20 | '-p', 21 | '--port', 22 | dest='port', 23 | nargs='?', 24 | const=53516, 25 | type=int, 26 | default=53516, 27 | help='Port number to be connected, by default it\'s 53516') 28 | args_in = parser.parse_args() 29 | 30 | 31 | def run_adb(args, pipeOutput=True): 32 | if(args_in.device_serial): 33 | args = adb + ['-s', args_in.device_serial] + args 34 | else: 35 | args = adb + args 36 | 37 | # print('exec cmd : %s' % args) 38 | out = None 39 | if (pipeOutput): 40 | out = subprocess.PIPE 41 | 42 | p = subprocess.Popen([str(arg) 43 | for arg in args], stdout=out) 44 | stdout, stderr = p.communicate() 45 | return (p.returncode, stdout, stderr) 46 | 47 | 48 | def locate_apk_path(): 49 | (rc, out, _) = run_adb(["shell", "pm", 50 | "path", 51 | "com.rayworks.droidcast"]) 52 | if rc or out == "": 53 | raise RuntimeError( 54 | "Locating apk failure, have you installed the app successfully?") 55 | 56 | prefix = "package:" 57 | postfix = ".apk" 58 | beg = out.index(prefix, 0) 59 | end = out.rfind(postfix) 60 | 61 | return "CLASSPATH=" + out[beg + len(prefix):(end + len(postfix))].strip() 62 | 63 | 64 | def open_browser(): 65 | url = 'http://localhost:%d/screenshot' % args_in.port 66 | webbrowser.open_new(url) 67 | 68 | 69 | def identify_device(): 70 | 71 | (rc, out, _) = run_adb(["devices"]) 72 | if(rc): 73 | raise RuntimeError("Fail to find devices") 74 | else: 75 | # Output as following: 76 | # List of devices attached 77 | # 6466eb0c device 78 | print out 79 | device_serial_no = args_in.device_serial 80 | 81 | devicesInfo = str(out) 82 | deviceCnt = devicesInfo.count('device') - 1 83 | 84 | if deviceCnt < 1: 85 | raise RuntimeError("Fail to find devices") 86 | 87 | if(deviceCnt > 1 and (not device_serial_no)): 88 | raise RuntimeError( 89 | "Please specify the serial number of target device you want to use ('-s serial_number').") 90 | 91 | 92 | def print_url(): 93 | # ip route: 94 | # e.g. 192.168.0.0/24 dev wlan0 proto kernel scope link src 192.168.0.125 95 | (rc, out, _) = run_adb( 96 | ["shell", "ip route | awk '/wlan*/{ print $9 }'| tr -d '\n'"]) 97 | print "\n>>> Share the url 'http://{0}:{1}/screenshot' to see the live screen! <<<\n".format(str(out), args_in.port) 98 | 99 | 100 | def automate(): 101 | try: 102 | identify_device() 103 | 104 | class_path = locate_apk_path() 105 | 106 | (code, _, err) = run_adb( 107 | ["forward", "tcp:%d" % args_in.port, "tcp:%d" % args_in.port]) 108 | print(">>> adb forward tcp:%d " % args_in.port, code) 109 | 110 | print_url() 111 | 112 | args = ["shell", 113 | class_path, 114 | "app_process", 115 | "/", # unused 116 | "com.rayworks.droidcast.Main", 117 | "--port=%d" % args_in.port] 118 | 119 | # delay opening the web page 120 | t = Timer(2, open_browser) 121 | t.start() 122 | 123 | # event loop starts 124 | run_adb(args, pipeOutput=False) 125 | 126 | (code, out, err) = run_adb( 127 | ["forward", "--remove", "tcp:%d" % args_in.port]) 128 | print(">>> adb unforward tcp:%d " % args_in.port, code) 129 | 130 | except (Exception) as e: 131 | print e 132 | 133 | 134 | if __name__ == "__main__": 135 | automate() 136 | -------------------------------------------------------------------------------- /scripts/automation3.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python -u 2 | 3 | # Script (generated for Python 3.6+) to automate the configurations to show the screenshot on your 4 | # default web browser. 5 | # To get started, simply run : 'python ./automation.py' 6 | 7 | import subprocess 8 | import webbrowser 9 | import argparse 10 | import signal 11 | 12 | from threading import Timer 13 | 14 | adb = ['adb'] 15 | 16 | parser = argparse.ArgumentParser( 17 | description='Automation script to activate capturing screenshot of Android device') 18 | parser.add_argument('-s', '--serial', dest='device_serial', 19 | help='Device serial number (adb -s option)') 20 | parser.add_argument( 21 | '-p', 22 | '--port', 23 | dest='port', 24 | nargs='?', 25 | const=53516, 26 | type=int, 27 | default=53516, 28 | help='Port number to be connected, by default it\'s 53516') 29 | args_in = parser.parse_args() 30 | 31 | 32 | def run_adb(args, pipeOutput=True): 33 | if(args_in.device_serial): 34 | args = adb + ['-s', args_in.device_serial] + args 35 | else: 36 | args = adb + args 37 | 38 | # print('exec cmd : %s' % args) 39 | out = None 40 | if (pipeOutput): 41 | out = subprocess.PIPE 42 | 43 | p = subprocess.Popen([str(arg) 44 | for arg in args], stdout=out, encoding='utf-8') 45 | stdout, stderr = p.communicate() 46 | return (p.returncode, stdout, stderr) 47 | 48 | 49 | def locate_apk_path(): 50 | (rc, out, _) = run_adb(["shell", "pm", 51 | "path", 52 | "com.rayworks.droidcast"]) 53 | if rc or out == "": 54 | raise RuntimeError( 55 | "Locating apk failure, have you installed the app successfully?") 56 | 57 | prefix = "package:" 58 | postfix = ".apk" 59 | beg = out.index(prefix, 0) 60 | end = out.rfind(postfix) 61 | 62 | return "CLASSPATH=" + out[beg + len(prefix):(end + len(postfix))].strip() 63 | 64 | 65 | def open_browser(): 66 | url = 'http://localhost:%d/screenshot' % args_in.port 67 | webbrowser.open_new(url) 68 | 69 | 70 | def identify_device(): 71 | 72 | (rc, out, _) = run_adb(["devices"]) 73 | if(rc): 74 | raise RuntimeError("Fail to find devices") 75 | else: 76 | # Output as following: 77 | # List of devices attached 78 | # 6466eb0c device 79 | print(out) 80 | device_serial_no = args_in.device_serial 81 | 82 | devicesInfo = str(out) 83 | deviceCnt = devicesInfo.count('device') - 1 84 | 85 | if deviceCnt < 1: 86 | raise RuntimeError("Fail to find devices") 87 | 88 | if(deviceCnt > 1 and (not device_serial_no)): 89 | raise RuntimeError( 90 | "Please specify the serial number of target device you want to use ('-s serial_number').") 91 | 92 | 93 | def print_url(): 94 | # ip route: 95 | # e.g. 192.168.0.0/24 dev wlan0 proto kernel scope link src 192.168.0.125 96 | (rc, out, _) = run_adb( 97 | ["shell", "ip route | awk '/wlan*/{ print $9 }'| tr -d '\n'"]) 98 | print( 99 | ("\n>>> Share the url 'http://%s:%d/screenshot' to see the live screen! <<<\n") % 100 | (out, args_in.port)) 101 | 102 | 103 | def handler(signum, frame): 104 | print('\n>>> Signal caught: ', signum) 105 | (code, out, err) = run_adb( 106 | ["forward", "--remove", "tcp:%d" % args_in.port]) 107 | print(">>> adb unforward tcp:%d " % args_in.port, code) 108 | 109 | 110 | def automate(): 111 | # handle the keyboard interruption explicitly 112 | signal.signal(signal.SIGINT, handler) 113 | 114 | try: 115 | identify_device() 116 | 117 | class_path = locate_apk_path() 118 | 119 | (code, _, err) = run_adb( 120 | ["forward", "tcp:%d" % args_in.port, "tcp:%d" % args_in.port]) 121 | print(">>> adb forward tcp:%d " % args_in.port, code) 122 | 123 | print_url() 124 | 125 | args = ["shell", 126 | class_path, 127 | "app_process", 128 | "/", # unused 129 | "com.rayworks.droidcast.Main", 130 | "--port=%d" % args_in.port] 131 | 132 | # delay opening the web page 133 | t = Timer(2, open_browser) 134 | t.start() 135 | 136 | # event loop starts 137 | run_adb(args, pipeOutput=False) 138 | 139 | except (Exception) as e: 140 | print(e) 141 | 142 | 143 | if __name__ == "__main__": 144 | automate() 145 | -------------------------------------------------------------------------------- /scripts/screencap_server.py: -------------------------------------------------------------------------------- 1 | # 2 | # The simple script to take a screenshot by the built-in ADB command directly (‘ adb screencap -p ’) 3 | # required : Python 3.10+, adb tool 4 | # 5 | # usage : 6 | # - use `python screencap_server.py` to start server 7 | # - open browser http://localhost:12347/screenshot to view the screenshot 8 | # 9 | import argparse 10 | import subprocess 11 | from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler 12 | from urllib import parse 13 | 14 | adb = ['adb'] 15 | 16 | parser = argparse.ArgumentParser( 17 | description='A simple script used to capture screenshot for Android devices') 18 | parser.add_argument('-s', '--serial', dest='device_serial', 19 | help='Device serial number (adb -s option)') 20 | parser.add_argument( 21 | '-p', 22 | '--port', 23 | dest='port', 24 | nargs='?', 25 | const=12347, 26 | type=int, 27 | default=12347, 28 | help='Port number to be connected, by default it\'s 12347') 29 | args_in = parser.parse_args() 30 | 31 | 32 | def run_adb(args, pipeOutput=True): 33 | if (args_in.device_serial): 34 | args = adb + ['-s', args_in.device_serial] + args 35 | else: 36 | args = adb + args 37 | 38 | out = None 39 | if (pipeOutput): 40 | out = subprocess.PIPE 41 | 42 | return subprocess.Popen([str(arg) for arg in args], stdout=out) 43 | 44 | 45 | class ScreenCapHandler(BaseHTTPRequestHandler): 46 | def capture_by_adb(self, out): 47 | """ capture the screenshot via adb tool """ 48 | process = run_adb(["shell", "screencap", "-p"]) 49 | screenshot_raw = process.stdout.read() 50 | out.write(screenshot_raw) 51 | print("screenshot generated by adb cmd") 52 | 53 | def do_GET(self): 54 | print("client addr : ", self.client_address) 55 | parsed = parse.urlparse(self.path) 56 | # print("parsed path : ", parsed.path) 57 | if parsed.path == '/screenshot': 58 | self.send_response(200) 59 | self.send_header("Content-Type", "image/jpeg") 60 | self.end_headers() 61 | 62 | self.capture_by_adb(self.wfile) 63 | else: 64 | self.send_response(500) 65 | self.send_header("Content-Type", "text/html") 66 | self.end_headers() 67 | 68 | self.wfile.write(str.encode('

Unsupported request

')) 69 | 70 | 71 | def identify_device(): 72 | proc = subprocess.Popen(['adb', "devices"], stdout=subprocess.PIPE) 73 | if proc.returncode: 74 | raise RuntimeError("Fail to find devices") 75 | else: 76 | # Output as following: 77 | # List of devices attached 78 | # 6466eb0c device 79 | device_serial_no = args_in.device_serial 80 | 81 | devices_info = str(proc.stdout.read()) 82 | print("device info : ", devices_info) 83 | device_cnt = devices_info.count('device') - 1 84 | 85 | if device_cnt < 1: 86 | raise RuntimeError("Fail to find devices") 87 | 88 | if device_cnt > 1 and (not device_serial_no): 89 | raise RuntimeError( 90 | "Please specify the serial number of target device you want to use ('-s serial_number').") 91 | 92 | 93 | if __name__ == "__main__": 94 | try: 95 | identify_device() 96 | 97 | http_svr = ThreadingHTTPServer( 98 | ("0.0.0.0", args_in.port), ScreenCapHandler) 99 | print("Server Port : ", str(http_svr.server_port)) 100 | http_svr.serve_forever() 101 | except Exception as e: 102 | print(e) 103 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # WebSocket test 2 | 3 | After [setting up the tool properly](../README.md#quick-start), you could also view the synchronous screenshot 4 | with `image.html` as the connected device gets rotated. 5 | -------------------------------------------------------------------------------- /web/image.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Screenshot 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Display the screenshot via WebSocket

13 | 14 |

*Tips: rotate your connected device to see the updated screenshot :)

15 | 16 |
17 | Droid screenshot 18 |
19 | 20 | 40 | 41 | 42 | 43 | 87 |
88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /web/styles/layout.css: -------------------------------------------------------------------------------- 1 | .centered-wrapper { 2 | position: relative; 3 | text-align: center; 4 | } 5 | 6 | .centered-wrapper:before { 7 | content: ""; 8 | position: relative; 9 | display: inline-block; 10 | width: 0; 11 | height: 100%; 12 | vertical-align: middle; 13 | } 14 | 15 | .centered-content { 16 | display: inline-block; 17 | vertical-align: middle; 18 | } 19 | 20 | body { 21 | font-family: "Helvetica", "Arial", serif; 22 | background-color: #0e0e0e; 23 | } 24 | 25 | h2 { 26 | color: #fff; 27 | } 28 | 29 | p { 30 | color: #fff; 31 | } -------------------------------------------------------------------------------- /web/util.js: -------------------------------------------------------------------------------- 1 | function encode(input) { 2 | var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 3 | var output = ""; 4 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 5 | var i = 0; 6 | 7 | while (i < input.length) { 8 | chr1 = input[i++]; 9 | chr2 = i < input.length ? input[i++] : Number.NaN; // Not sure if the index 10 | chr3 = i < input.length ? input[i++] : Number.NaN; // checks are needed here 11 | 12 | enc1 = chr1 >> 2; 13 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 14 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 15 | enc4 = chr3 & 63; 16 | 17 | if (isNaN(chr2)) { 18 | enc3 = enc4 = 64; 19 | } else if (isNaN(chr3)) { 20 | enc4 = 64; 21 | } 22 | output += keyStr.charAt(enc1) + keyStr.charAt(enc2) + 23 | keyStr.charAt(enc3) + keyStr.charAt(enc4); 24 | } 25 | return output; 26 | } --------------------------------------------------------------------------------