├── .gitignore ├── LICENCE ├── README.md ├── build.gradle ├── device-api └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── cosysoft │ │ │ └── device │ │ │ ├── DeviceStore.java │ │ │ ├── android │ │ │ ├── AndroidApp.java │ │ │ ├── AndroidDevice.java │ │ │ ├── AndroidDeviceBrand.java │ │ │ ├── DeviceTargetPlatform.java │ │ │ ├── KeyEvent.java │ │ │ ├── impl │ │ │ │ ├── AbstractDevice.java │ │ │ │ ├── AndroidDeviceStore.java │ │ │ │ ├── DefaultAndroidApp.java │ │ │ │ ├── DefaultHardwareDevice.java │ │ │ │ ├── DeviceChangeListener.java │ │ │ │ └── InstalledAndroidApp.java │ │ │ └── xiaomi │ │ │ │ ├── MIDeviceUtility.java │ │ │ │ ├── MIInstaller.java │ │ │ │ └── MIJuger.java │ │ │ ├── exception │ │ │ ├── AndroidDeviceException.java │ │ │ ├── DeviceNotFoundException.java │ │ │ ├── DeviceStoreException.java │ │ │ ├── DeviceUnlockException.java │ │ │ ├── NestedException.java │ │ │ └── NestedExceptionUtils.java │ │ │ ├── image │ │ │ ├── ImageUtils.java │ │ │ ├── SixteenBitColorModel.java │ │ │ └── ThirtyTwoBitColorModel.java │ │ │ ├── model │ │ │ ├── AppInfo.java │ │ │ ├── ClientDataInfo.java │ │ │ └── DeviceInfo.java │ │ │ └── shell │ │ │ ├── AndroidSdk.java │ │ │ ├── AndroidSdkException.java │ │ │ ├── OS.java │ │ │ ├── ShellCommand.java │ │ │ └── ShellCommandException.java │ └── resources │ │ └── com │ │ └── github │ │ └── cosysoft │ │ └── device │ │ └── android │ │ ├── impl │ │ ├── automator.jar │ │ └── handlePopBox.jar │ │ ├── unlock_apk-debug.apk │ │ └── xiaomi │ │ └── CapMI_1.0.apk │ └── test │ └── java │ └── com │ └── github │ └── cosysoft │ └── device │ └── android │ └── test │ ├── AndroidAppTest.java │ ├── AndroidDeviceTest.java │ ├── DeviceClientTest.java │ ├── DeviceTest.java │ ├── LogcatTest.java │ ├── PerformanceTest.java │ ├── Readme.java │ └── XiaomiInstallerTest.java ├── device-keeper ├── .bowerrc ├── app │ ├── app.css │ ├── app.js │ ├── controller.js │ ├── device │ │ ├── device-list.html │ │ └── node-list.html │ ├── i2.html │ ├── index.html │ └── mock │ │ └── avatar.png ├── bower.json ├── keeper │ └── device.js ├── main.js └── package.json ├── device-node └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── cosysoft │ │ │ └── device │ │ │ └── node │ │ │ ├── Application.java │ │ │ ├── config │ │ │ ├── CacheConfig.java │ │ │ ├── JacksonMapperConfig.java │ │ │ └── WebConfig.java │ │ │ ├── controller │ │ │ ├── DeviceController.java │ │ │ └── MasterController.java │ │ │ ├── domain │ │ │ ├── Device.java │ │ │ └── Result.java │ │ │ ├── service │ │ │ ├── DeviceService.java │ │ │ ├── MasterDeviceService.java │ │ │ └── NodeService.java │ │ │ └── task │ │ │ ├── DriveStaleDevice.java │ │ │ └── NodeRegister.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── com │ └── github │ └── cosysoft │ └── device │ └── node │ └── test │ └── ApplicationTest.java ├── gradle.properties ├── gradle ├── publishInternalNexus.gradle └── upload.gradle └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 2 | hs_err_pid* 3 | 4 | # Package Files 5 | *.jar 6 | *.war 7 | *.ear 8 | 9 | *.project 10 | *.class 11 | *.classpath 12 | *.log 13 | target 14 | bin 15 | build 16 | test-output 17 | .settings 18 | 19 | 20 | *.DS_Store 21 | 22 | .idea 23 | out 24 | *.iml 25 | *.ipr 26 | *.iws 27 | 28 | bower_components/ 29 | node_modules/ 30 | 31 | .gradle -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2012-2014 eBay Software Foundation and selendroid committers and device comitters 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | “[device] is being sponsored by the following tool; please help to support us by taking a look and signing up to a free trial” 2 | 3 | 4 | ## Android Device API Based on ddmlib 5 | 6 | A lot of code quote from selendroid,but we will foucs on simplify ddmlib's usage 7 | 8 | ## device-keeper 9 | A distributed android device monitor system based on device-api 10 | 11 | ### Quick Start 12 | 13 | You require the following to build: 14 | 15 | * Latest stable [Oracle JDK 7+](http://www.oracle.com/technetwork/java/) 16 | * Latest stable [Gradle 2.4+](http://gradle.org/downloads/) 17 | * Android SDK 18 | * node.js and bower 19 | 20 | And be sure that JAVA_HOME,ANDROID_HOME at your environment path. 21 | 22 | 23 | Plug a android device via usb or boot an emulator 24 | 25 | ```bash 26 | git clone https://github.com/cosysoft/device.git 27 | cd device/device-keeper 28 | bower install 29 | 30 | cd .. 31 | gradle bootRun 32 | ``` 33 | Open in your browser 34 | 35 | ## device-api 36 | Focus on stabilized android device operation via Android Debug Bridge 37 | 38 | ### Quick Start 39 | #### Download 40 | Maven 41 | ```xml 42 | 43 | com.github.cosysoft 44 | device-api 45 | 0.9.3 46 | 47 | ``` 48 | Gradle 49 | ```groovy 50 | dependencies { 51 | compile 'com.github.cosysoft:device-api:0.9.3' 52 | } 53 | ``` 54 | 55 | 56 | ### Take Devices 57 | 58 | ```java 59 | TreeSet devices = AndroidDeviceStore.getInstance() 60 | .getDevices(); 61 | 62 | for (AndroidDevice d : devices) { 63 | System.out.println(d.getSerialNumber()); 64 | } 65 | AndroidDevice device = devices.pollFirst(); 66 | System.out.println(device.getName()); 67 | ``` 68 | 69 | ### Screenshot 70 | 71 | ```java 72 | BufferedImage image = device.takeScreenshot(); 73 | String imagePath = new File(System.getProperty("java.io.tmpdir"), 74 | "screenshot.png").getAbsolutePath(); 75 | ImageUtils.writeToFile(image, imagePath); 76 | ``` 77 | 78 | ### Install/Uninstall App 79 | 80 | ```java 81 | AndroidApp app = new DefaultAndroidApp(new File( 82 | "d:\\uat\\com.android.chrome.apk")); 83 | device.install(app); 84 | if (device.isInstalled(app)) { 85 | device.uninstall(app); 86 | } 87 | ``` 88 | 89 | ### LogCat with custom filter 90 | ```java 91 | final LogCatFilter filter = new LogCatFilter("", "", "com.android", "", 92 | "", LogLevel.WARN); 93 | final LogCatListener lcl = new LogCatListener() { 94 | @Override 95 | public void log(List msgList) { 96 | for (LogCatMessage msg : msgList) { 97 | if (filter.matches(msg)) { 98 | System.out.println(msg); 99 | } 100 | } 101 | } 102 | }; 103 | 104 | device.addLogCatListener(lcl); 105 | Thread.sleep(60000); 106 | ``` 107 | 108 | ## Monitor 109 | Ddmlib can monitor one app's cpu/heap/threads and much more,but we need list running client first. 110 | 111 | ### List running client for app 112 | ```java 113 | @Test 114 | public void testListClients() { 115 | 116 | Client[] clients = device.getAllClient(); 117 | for (Client client : clients) { 118 | ClientData clientData = client.getClientData(); 119 | System.out.println(clientData.getClientDescription() + " " + clientData.getPid()); 120 | } 121 | } 122 | 123 | ``` 124 | ### List selected app threads 125 | ```java 126 | 127 | @Test 128 | public void testListTheads() { 129 | 130 | Client runningApp = device.getClientByAppName("com.android.calendar"); 131 | 132 | ThreadInfo[] threads = runningApp.getClientData().getThreads(); 133 | 134 | for (int i = 0; i < threads.length; i++) { 135 | System.out.println(threads[i].getThreadName() 136 | + " at " 137 | + threads[i].getStatus()); 138 | } 139 | } 140 | ``` 141 | ## License 142 | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 143 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { 4 | 5 | url 'http://maven.dev.sh.ctripcorp.com:8081/nexus/content/groups/public/' 6 | } 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.7.RELEASE") 10 | } 11 | } 12 | 13 | apply plugin: 'idea' 14 | 15 | 16 | idea { 17 | project { 18 | languageLevel = '1.7' 19 | } 20 | } 21 | 22 | 23 | subprojects { 24 | version '0.9.3' 25 | apply plugin: 'java' 26 | apply plugin: 'idea' 27 | 28 | 29 | 30 | sourceCompatibility = 1.7 31 | targetCompatibility = 1.7 32 | 33 | repositories { 34 | maven { 35 | url 'http://maven.dev.sh.ctripcorp.com:8081/nexus/content/groups/public/' 36 | } 37 | mavenCentral() 38 | } 39 | dependencies { 40 | 41 | compile 'org.slf4j:slf4j-api:1.7.12' 42 | compile 'org.slf4j:slf4j-ext:1.7.12' 43 | compile 'ch.qos.logback:logback-classic:1.1.3' 44 | compile 'commons-io:commons-io:2.4' 45 | compile 'org.apache.commons:commons-lang3:3.4' 46 | testCompile("junit:junit:4.12") 47 | } 48 | compileJava { 49 | 50 | options.encoding = 'utf-8' 51 | //enable incremental compilation 52 | } 53 | javadoc { 54 | options { 55 | locale = 'en_US' 56 | encoding = 'UTF-8' 57 | } 58 | } 59 | } 60 | 61 | project(':device-api') { 62 | 63 | ext { 64 | ddmlibVersion = '24.5.0' 65 | } 66 | dependencies { 67 | compile 'org.apache.commons:commons-exec:1.2' 68 | compile 'commons-cli:commons-cli:1.2' 69 | compile "com.android.tools.ddms:ddmlib:$ddmlibVersion" 70 | compile "com.android.tools:dvlib:$ddmlibVersion" 71 | compile "com.android.tools:sdklib:$ddmlibVersion" 72 | compile "com.android.tools:sdk-common:$ddmlibVersion" 73 | } 74 | 75 | apply plugin: 'maven' 76 | 77 | group = "com.github.cosysoft" 78 | archivesBaseName = "device-api" 79 | 80 | task javadocJar(type: Jar, dependsOn: javadoc) { 81 | classifier = 'javadoc' 82 | from 'build/docs/javadoc' 83 | } 84 | 85 | task sourcesJar(type: Jar) { 86 | classifier = 'sources' 87 | from sourceSets.main.allSource 88 | } 89 | 90 | artifacts { 91 | archives jar 92 | archives javadocJar 93 | archives sourcesJar 94 | } 95 | 96 | if (rootProject.hasProperty('ossrhUsername')) { 97 | apply from: "$rootProject.projectDir/gradle/upload.gradle" 98 | } 99 | 100 | if (rootProject.hasProperty('inexusPassword')) { 101 | apply from: "$rootProject.projectDir/gradle/publishInternalNexus.gradle" 102 | } 103 | } 104 | 105 | project(':device-node') { 106 | 107 | /* sourceSets { 108 | main { 109 | resources { 110 | srcDir 'src/main/resources' 111 | include '*.properties' 112 | 113 | srcDir '../device-keeper' 114 | include 'app**' 115 | } 116 | } 117 | }*/ 118 | 119 | task copyKeeper(type: Copy) { 120 | from('../device-keeper/app') 121 | into(new File(sourceSets.main.output.resourcesDir, 'app')) 122 | } 123 | processResources.dependsOn copyKeeper 124 | 125 | apply plugin: 'spring-boot' 126 | 127 | bootRun { 128 | addResources = false 129 | } 130 | dependencies { 131 | compile project(':device-api') 132 | compile("org.springframework.boot:spring-boot-starter-web") 133 | compile("org.springframework.boot:spring-boot-starter-actuator") 134 | compile("org.springframework.boot:spring-boot-starter-remote-shell") 135 | testCompile("org.springframework.boot:spring-boot-starter-test") 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/DeviceStore.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device; 2 | 3 | import java.util.TreeSet; 4 | 5 | import com.github.cosysoft.device.android.AndroidDevice; 6 | 7 | /** 8 | * main class for phone resouces take and release 9 | * 10 | * @author ltyao 11 | */ 12 | public interface DeviceStore { 13 | 14 | void shutdown(); 15 | 16 | void shutdownForcely(); 17 | 18 | /** 19 | * internal usage 20 | * 21 | * @return 22 | */ 23 | TreeSet getDevices(); 24 | 25 | AndroidDevice getDeviceBySerial(String serialID); 26 | } 27 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/AndroidApp.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android; 2 | 3 | 4 | public interface AndroidApp { 5 | 6 | String getBasePackage(); 7 | 8 | String getMainActivity(); 9 | 10 | void setMainActivity(String mainActivity); 11 | 12 | String getVersionName(); 13 | 14 | void deleteFileFromWithinApk(String file); 15 | 16 | String getAppId(); 17 | 18 | /** 19 | * For testing only 20 | */ 21 | String getAbsolutePath(); 22 | } 23 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/AndroidDevice.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android; 2 | 3 | import com.android.ddmlib.Client; 4 | import com.android.ddmlib.IDevice; 5 | import com.android.ddmlib.logcat.LogCatListener; 6 | import com.github.cosysoft.device.model.ClientDataInfo; 7 | import com.github.cosysoft.device.model.DeviceInfo; 8 | import java.awt.Dimension; 9 | import java.awt.image.BufferedImage; 10 | import java.util.List; 11 | import java.util.Locale; 12 | 13 | /** 14 | * @author ltyao 15 | */ 16 | public interface AndroidDevice { 17 | 18 | /** 19 | * test only 20 | */ 21 | IDevice getDevice(); 22 | 23 | String getSerialNumber(); 24 | 25 | Locale getLocale(); 26 | 27 | String getName(); 28 | 29 | AndroidDeviceBrand getBrand(); 30 | 31 | Dimension getScreenSize(); 32 | 33 | void tap(int x, int y); 34 | 35 | void swipe(int x1, int y1, int x2, int y2); 36 | 37 | /** 38 | * @see KeyEvent 39 | */ 40 | void inputKeyevent(int value); 41 | 42 | BufferedImage takeScreenshot(); 43 | 44 | void takeScreenshot(String fileUrl); 45 | 46 | boolean isDeviceReady(); 47 | 48 | boolean isScreenOn(); 49 | 50 | DeviceTargetPlatform getTargetPlatform(); 51 | 52 | String currentActivity(); 53 | 54 | void invokeActivity(String activity); 55 | 56 | boolean start(AndroidApp app); 57 | 58 | /** 59 | * dump current activity view xml 60 | */ 61 | String getDump(); 62 | 63 | boolean handlePopBox(String deviceBrand); 64 | 65 | /** 66 | * io.appium.unlock/.Unlock 67 | */ 68 | void unlock(); 69 | 70 | void install(AndroidApp app); 71 | 72 | boolean isInstalled(String appBasePackage); 73 | 74 | boolean isInstalled(AndroidApp app); 75 | 76 | void uninstall(AndroidApp app); 77 | 78 | void uninstall(String appBasePackage); 79 | 80 | void forwardPort(int local, int remote); 81 | 82 | void removeForwardPort(int local); 83 | 84 | void clearUserData(AndroidApp app); 85 | 86 | void clearUserData(String appBasePackage); 87 | 88 | void kill(AndroidApp aut); 89 | 90 | void kill(String appBasePackage); 91 | 92 | String runAdbCommand(String parameter); 93 | 94 | String getExternalStoragePath(); 95 | 96 | String getCrashLog(); 97 | 98 | boolean isWifiOff(); 99 | 100 | DeviceInfo getDeviceInfo(); 101 | 102 | void restartADB(); 103 | 104 | /** 105 | * 106 | * @param logCatListener 107 | */ 108 | void addLogCatListener(LogCatListener logCatListener); 109 | 110 | void removeLogCatListener(LogCatListener logCatListener); 111 | 112 | List getClientDatasInfo(); 113 | 114 | /** 115 | * return all Dalvik/ART VM processes 116 | */ 117 | Client[] getAllClient(); 118 | 119 | /** 120 | * 121 | * @param appName 122 | * @return 123 | */ 124 | Client getClientByAppName(String appName); 125 | } 126 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/AndroidDeviceBrand.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * @author ltyao 8 | */ 9 | public final class AndroidDeviceBrand { 10 | 11 | private static final List brands = new ArrayList(); 12 | 13 | public static final AndroidDeviceBrand XIAOMI_MI_3W = createInstance( 14 | "xiaomi", "mi_3w"); 15 | public static final AndroidDeviceBrand XIAOMI_MI_2 = createInstance( 16 | "xiaomi", "mi_2"); 17 | public static final AndroidDeviceBrand XIAOMI_MI_3 = createInstance( 18 | "xiaomi", "mi_3"); 19 | public static final AndroidDeviceBrand XIAOMI_MI_4W = createInstance( 20 | "xiaomi", "mi_4w"); 21 | public static final AndroidDeviceBrand OPPO_X9007 = createInstance("oppo", 22 | "x9007"); 23 | public static final AndroidDeviceBrand MEIZU_M355 = createInstance("meizu", 24 | "m355"); 25 | public static final AndroidDeviceBrand HTC_M8ST = createInstance("htc", 26 | "htc_m8st"); 27 | 28 | public static final AndroidDeviceBrand EMPTY = createInstance("", ""); 29 | 30 | private String manufacture; 31 | private String model; 32 | 33 | public String getModel() { 34 | return model; 35 | } 36 | 37 | public void setModel(String model) { 38 | this.model = model; 39 | } 40 | 41 | public String getManufacture() { 42 | return manufacture; 43 | } 44 | 45 | public void setManufacture(String manufacture) { 46 | this.manufacture = manufacture; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "AndroidDeviceBrand [manufacture=" + manufacture + ", model=" 52 | + model + "]"; 53 | } 54 | 55 | private AndroidDeviceBrand(String manufacture, String model) { 56 | this.manufacture = manufacture; 57 | this.model = model; 58 | } 59 | 60 | public boolean isXiaoMi() { 61 | return this.equals(AndroidDeviceBrand.XIAOMI_MI_2) 62 | || this.equals(AndroidDeviceBrand.XIAOMI_MI_3) 63 | || this.equals(AndroidDeviceBrand.XIAOMI_MI_3W) || this.equals( 64 | AndroidDeviceBrand.XIAOMI_MI_4W); 65 | } 66 | 67 | private static AndroidDeviceBrand createInstance(String manufacture, 68 | String model) { 69 | AndroidDeviceBrand brand = new AndroidDeviceBrand(manufacture, model); 70 | brands.add(brand); 71 | return brand; 72 | } 73 | 74 | public static AndroidDeviceBrand from(String manufacture, String model) { 75 | for (AndroidDeviceBrand brand : brands) { 76 | if (brand.getManufacture().equals(manufacture) 77 | && brand.getModel().equals(model)) { 78 | return brand; 79 | } 80 | } 81 | return EMPTY; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/DeviceTargetPlatform.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2014 eBay Software Foundation and selendroid committers. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 | * in compliance with the License. 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 distributed under the License 10 | * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 | * or implied. See the License for the specific language governing permissions and limitations under 12 | * the License. 13 | */ 14 | package com.github.cosysoft.device.android; 15 | 16 | public enum DeviceTargetPlatform { 17 | ANDROID10("2.3.3"), ANDROID11("3.0"), ANDROID12("3.1"), ANDROID13("3.2"), ANDROID14( 18 | "4.0"), ANDROID15("4.0.3"), ANDROID16("4.1.2"), ANDROID17("4.2.2"), ANDROID18( 19 | "4.3"), ANDROID19("4.4"), ANDROID20("4.4W"), ANDROID21("5.0"), ANDROID22("5.1"), 20 | ANDROID23("6.0"); 21 | public static final String ANDROID = "ANDROID"; 22 | 23 | private String versionNumber; 24 | private String api; 25 | 26 | DeviceTargetPlatform(String version) { 27 | this.versionNumber = version; 28 | this.api = this.name().replace(ANDROID, ""); 29 | } 30 | 31 | public String getSdkFolderName() { 32 | return name().replace(ANDROID, "android-"); 33 | } 34 | 35 | public static DeviceTargetPlatform fromPlatformVersion(String text) { 36 | if (text != null) { 37 | for (DeviceTargetPlatform b : DeviceTargetPlatform.values()) { 38 | if (b.name().equals(ANDROID + text) || b.name().equals(text)) { 39 | return b; 40 | } 41 | } 42 | } 43 | return null; 44 | } 45 | 46 | public static DeviceTargetPlatform fromInt(String text) { 47 | if (text != null) { 48 | for (DeviceTargetPlatform b : DeviceTargetPlatform.values()) { 49 | if (b.name().equals(ANDROID + text)) { 50 | return b; 51 | } 52 | } 53 | } 54 | return null; 55 | } 56 | 57 | /** 58 | * @return version number of OS displayed on the device. 59 | */ 60 | public String getVersionNumber() { 61 | return versionNumber; 62 | } 63 | 64 | /** 65 | * @return api number, used in #{SelendroidCapabilities#PLATFORM_VERSION} 66 | */ 67 | public String getApi() { 68 | return api; 69 | } 70 | 71 | public String formatedName() { 72 | return this.name() + "(" + this.versionNumber + ")"; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/KeyEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android; 2 | 3 | /** 4 | * Copy of the public static Key Codes from: 5 | * http://developer.android.com/reference/android/view/KeyEvent.html 6 | */ 7 | public interface KeyEvent { 8 | public static final int KEYCODE_UNKNOWN = 0; 9 | public static final int KEYCODE_SOFT_LEFT = 1; 10 | public static final int KEYCODE_SOFT_RIGHT = 2; 11 | public static final int KEYCODE_HOME = 3; 12 | public static final int KEYCODE_BACK = 4; 13 | public static final int KEYCODE_CALL = 5; 14 | public static final int KEYCODE_ENDCALL = 6; 15 | public static final int KEYCODE_0 = 7; 16 | public static final int KEYCODE_1 = 8; 17 | public static final int KEYCODE_2 = 9; 18 | public static final int KEYCODE_3 = 10; 19 | public static final int KEYCODE_4 = 11; 20 | public static final int KEYCODE_5 = 12; 21 | public static final int KEYCODE_6 = 13; 22 | public static final int KEYCODE_7 = 14; 23 | public static final int KEYCODE_8 = 15; 24 | public static final int KEYCODE_9 = 16; 25 | public static final int KEYCODE_STAR = 17; 26 | public static final int KEYCODE_POUND = 18; 27 | public static final int KEYCODE_DPAD_UP = 19; 28 | public static final int KEYCODE_DPAD_DOWN = 20; 29 | public static final int KEYCODE_DPAD_LEFT = 21; 30 | public static final int KEYCODE_DPAD_RIGHT = 22; 31 | public static final int KEYCODE_DPAD_CENTER = 23; 32 | public static final int KEYCODE_VOLUME_UP = 24; 33 | public static final int KEYCODE_VOLUME_DOWN = 25; 34 | public static final int KEYCODE_POWER = 26; 35 | public static final int KEYCODE_CAMERA = 27; 36 | public static final int KEYCODE_CLEAR = 28; 37 | public static final int KEYCODE_A = 29; 38 | public static final int KEYCODE_B = 30; 39 | public static final int KEYCODE_C = 31; 40 | public static final int KEYCODE_D = 32; 41 | public static final int KEYCODE_E = 33; 42 | public static final int KEYCODE_F = 34; 43 | public static final int KEYCODE_G = 35; 44 | public static final int KEYCODE_H = 36; 45 | public static final int KEYCODE_I = 37; 46 | public static final int KEYCODE_J = 38; 47 | public static final int KEYCODE_K = 39; 48 | public static final int KEYCODE_L = 40; 49 | public static final int KEYCODE_M = 41; 50 | public static final int KEYCODE_N = 42; 51 | public static final int KEYCODE_O = 43; 52 | public static final int KEYCODE_P = 44; 53 | public static final int KEYCODE_Q = 45; 54 | public static final int KEYCODE_R = 46; 55 | public static final int KEYCODE_S = 47; 56 | public static final int KEYCODE_T = 48; 57 | public static final int KEYCODE_U = 49; 58 | public static final int KEYCODE_V = 50; 59 | public static final int KEYCODE_W = 51; 60 | public static final int KEYCODE_X = 52; 61 | public static final int KEYCODE_Y = 53; 62 | public static final int KEYCODE_Z = 54; 63 | public static final int KEYCODE_COMMA = 55; 64 | public static final int KEYCODE_PERIOD = 56; 65 | public static final int KEYCODE_ALT_LEFT = 57; 66 | public static final int KEYCODE_ALT_RIGHT = 58; 67 | public static final int KEYCODE_SHIFT_LEFT = 59; 68 | public static final int KEYCODE_SHIFT_RIGHT = 60; 69 | public static final int KEYCODE_TAB = 61; 70 | public static final int KEYCODE_SPACE = 62; 71 | public static final int KEYCODE_SYM = 63; 72 | public static final int KEYCODE_EXPLORER = 64; 73 | public static final int KEYCODE_ENVELOPE = 65; 74 | public static final int KEYCODE_ENTER = 66; 75 | public static final int KEYCODE_DEL = 67; 76 | public static final int KEYCODE_GRAVE = 68; 77 | public static final int KEYCODE_MINUS = 69; 78 | public static final int KEYCODE_EQUALS = 70; 79 | public static final int KEYCODE_LEFT_BRACKET = 71; 80 | public static final int KEYCODE_RIGHT_BRACKET = 72; 81 | public static final int KEYCODE_BACKSLASH = 73; 82 | public static final int KEYCODE_SEMICOLON = 74; 83 | public static final int KEYCODE_APOSTROPHE = 75; 84 | public static final int KEYCODE_SLASH = 76; 85 | public static final int KEYCODE_AT = 77; 86 | public static final int KEYCODE_NUM = 78; 87 | public static final int KEYCODE_HEADSETHOOK = 79; 88 | public static final int KEYCODE_FOCUS = 80; 89 | public static final int KEYCODE_PLUS = 81; 90 | public static final int KEYCODE_MENU = 82; 91 | public static final int KEYCODE_NOTIFICATION = 83; 92 | public static final int KEYCODE_SEARCH = 84; 93 | public static final int KEYCODE_MEDIA_PLAY_PAUSE = 85; 94 | public static final int KEYCODE_MEDIA_STOP = 86; 95 | public static final int KEYCODE_MEDIA_NEXT = 87; 96 | public static final int KEYCODE_MEDIA_PREVIOUS = 88; 97 | public static final int KEYCODE_MEDIA_REWIND = 89; 98 | public static final int KEYCODE_MEDIA_FAST_FORWARD = 90; 99 | public static final int KEYCODE_MUTE = 91; 100 | public static final int KEYCODE_PAGE_UP = 92; 101 | public static final int KEYCODE_PAGE_DOWN = 93; 102 | public static final int KEYCODE_PICTSYMBOLS = 94; 103 | public static final int KEYCODE_SWITCH_CHARSET = 95; 104 | public static final int KEYCODE_BUTTON_A = 96; 105 | public static final int KEYCODE_BUTTON_B = 97; 106 | public static final int KEYCODE_BUTTON_C = 98; 107 | public static final int KEYCODE_BUTTON_X = 99; 108 | public static final int KEYCODE_BUTTON_Y = 100; 109 | public static final int KEYCODE_BUTTON_Z = 101; 110 | public static final int KEYCODE_BUTTON_L1 = 102; 111 | public static final int KEYCODE_BUTTON_R1 = 103; 112 | public static final int KEYCODE_BUTTON_L2 = 104; 113 | public static final int KEYCODE_BUTTON_R2 = 105; 114 | public static final int KEYCODE_BUTTON_THUMBL = 106; 115 | public static final int KEYCODE_BUTTON_THUMBR = 107; 116 | public static final int KEYCODE_BUTTON_START = 108; 117 | public static final int KEYCODE_BUTTON_SELECT = 109; 118 | public static final int KEYCODE_BUTTON_MODE = 110; 119 | public static final int MAX_KEYCODE = 84; 120 | public static final int ACTION_DOWN = 0; 121 | public static final int ACTION_UP = 1; 122 | public static final int ACTION_MULTIPLE = 2; 123 | public static final int META_ALT_ON = 2; 124 | public static final int META_ALT_LEFT_ON = 16; 125 | public static final int META_ALT_RIGHT_ON = 32; 126 | public static final int META_SHIFT_ON = 1; 127 | public static final int META_SHIFT_LEFT_ON = 64; 128 | public static final int META_SHIFT_RIGHT_ON = 128; 129 | public static final int META_SYM_ON = 4; 130 | public static final int FLAG_WOKE_HERE = 1; 131 | public static final int FLAG_SOFT_KEYBOARD = 2; 132 | public static final int FLAG_KEEP_TOUCH_MODE = 4; 133 | public static final int FLAG_FROM_SYSTEM = 8; 134 | public static final int FLAG_EDITOR_ACTION = 16; 135 | public static final int FLAG_CANCELED = 32; 136 | public static final int FLAG_VIRTUAL_HARD_KEY = 64; 137 | public static final int FLAG_LONG_PRESS = 128; 138 | public static final int FLAG_CANCELED_LONG_PRESS = 256; 139 | public static final int FLAG_TRACKING = 512; 140 | } 141 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/impl/AbstractDevice.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.impl; 2 | 3 | import com.android.ddmlib.AdbCommandRejectedException; 4 | import com.android.ddmlib.Client; 5 | import com.android.ddmlib.IDevice; 6 | import com.android.ddmlib.RawImage; 7 | import com.android.ddmlib.TimeoutException; 8 | import com.android.ddmlib.logcat.LogCatListener; 9 | import com.android.ddmlib.logcat.LogCatReceiverTask; 10 | import com.github.cosysoft.device.android.AndroidApp; 11 | import com.github.cosysoft.device.android.AndroidDevice; 12 | import com.github.cosysoft.device.android.AndroidDeviceBrand; 13 | import com.github.cosysoft.device.android.DeviceTargetPlatform; 14 | import com.github.cosysoft.device.exception.AndroidDeviceException; 15 | import com.github.cosysoft.device.exception.DeviceUnlockException; 16 | import com.github.cosysoft.device.exception.NestedException; 17 | import com.github.cosysoft.device.image.ImageUtils; 18 | import com.github.cosysoft.device.model.ClientDataInfo; 19 | import com.github.cosysoft.device.model.DeviceInfo; 20 | import com.github.cosysoft.device.shell.AndroidSdk; 21 | import com.github.cosysoft.device.shell.AndroidSdkException; 22 | import com.github.cosysoft.device.shell.ShellCommand; 23 | import com.github.cosysoft.device.shell.ShellCommandException; 24 | import java.awt.image.BufferedImage; 25 | import java.io.BufferedReader; 26 | import java.io.File; 27 | import java.io.FileInputStream; 28 | import java.io.FileNotFoundException; 29 | import java.io.IOException; 30 | import java.io.InputStream; 31 | import java.io.InputStreamReader; 32 | import java.io.StringReader; 33 | import java.util.ArrayList; 34 | import java.util.HashSet; 35 | import java.util.List; 36 | import java.util.Set; 37 | import java.util.concurrent.ExecutionException; 38 | import java.util.regex.Matcher; 39 | import java.util.regex.Pattern; 40 | import org.apache.commons.exec.CommandLine; 41 | import org.apache.commons.io.FileUtils; 42 | import org.apache.commons.io.IOUtils; 43 | import org.apache.commons.lang3.StringUtils; 44 | import org.slf4j.Logger; 45 | import org.slf4j.LoggerFactory; 46 | import org.slf4j.profiler.Profiler; 47 | 48 | public abstract class AbstractDevice implements AndroidDevice { 49 | private static final Logger log = LoggerFactory 50 | .getLogger(AbstractDevice.class); 51 | protected String serial = null; 52 | protected IDevice device; 53 | private static final Integer COMMAND_TIMEOUT = 20000; 54 | private AndroidDeviceBrand brand = null; 55 | 56 | /** 57 | * Constructor meant to be used with Android Emulators because a reference to the {@link IDevice} 58 | * will become available if the emulator will be started. Please make sure that #setIDevice is 59 | * called on the emulator. 60 | */ 61 | public AbstractDevice(String serial) { 62 | this.serial = serial; 63 | } 64 | 65 | /** 66 | * Constructor mean to be used with Android Hardware devices because a reference to the {@link 67 | * IDevice} will be available immediately after they are connected. 68 | */ 69 | public AbstractDevice(IDevice device) { 70 | this.device = device; 71 | this.serial = device.getSerialNumber(); 72 | } 73 | 74 | @Override 75 | public IDevice getDevice() { 76 | return device; 77 | } 78 | 79 | protected AbstractDevice() { 80 | } 81 | 82 | protected boolean isSerialConfigured() { 83 | return serial != null && !serial.isEmpty(); 84 | } 85 | 86 | @Override 87 | public boolean isDeviceReady() { 88 | CommandLine command = adbCommand("shell", "getprop init.svc.bootanim"); 89 | String bootAnimDisplayed = null; 90 | try { 91 | bootAnimDisplayed = ShellCommand.exec(command); 92 | } catch (ShellCommandException e) { 93 | log.info("Could not get property init.svc.bootanim", e); 94 | } 95 | return bootAnimDisplayed != null 96 | && bootAnimDisplayed.contains("stopped"); 97 | } 98 | 99 | /** 100 | * ugly implementation 101 | */ 102 | @Override 103 | public boolean isScreenOn() { 104 | CommandLine command = adbCommand("shell", "dumpsys power"); 105 | try { 106 | String powerState = ShellCommand.exec(command).toLowerCase(); 107 | if (powerState.indexOf("mscreenon=true") > -1 108 | || powerState.indexOf("mpowerstate=0") == -1) { 109 | return true; 110 | } 111 | } catch (ShellCommandException e) { 112 | log.info("Could not get property init.svc.bootanim", e); 113 | } 114 | return false; 115 | } 116 | 117 | ; 118 | 119 | @Override 120 | public String currentActivity() { 121 | 122 | CommandLine command = adbCommand("shell", "dumpsys activity top"); 123 | 124 | String out = executeCommandQuietly(command); 125 | if (out.indexOf("ACTIVITY") > -1) { 126 | try { 127 | List lines = IOUtils.readLines(new StringReader(out)); 128 | for (String line : lines) { 129 | if (line.contains("ACTIVITY")) { 130 | String[] tokens = StringUtils.split(line, " "); 131 | return tokens[1]; 132 | } 133 | } 134 | } catch (IOException e) { 135 | log.debug("currentActivity {}", out); 136 | } 137 | } 138 | throw new NestedException("Can't get currentActivity"); 139 | } 140 | 141 | @Override 142 | public void unlock() { 143 | String unlockPackage = "ctrip.cap.mi"; 144 | String activity = ".CapMI"; 145 | 146 | if (this.isInstalled(unlockPackage)) { 147 | innerUnlock(unlockPackage, activity); 148 | return; 149 | } 150 | 151 | unlockPackage = "io.appium.unlock"; 152 | activity = ".Unlock"; 153 | 154 | if (!this.isInstalled(unlockPackage)) { 155 | throw new DeviceUnlockException( 156 | "UnLock app not installed on your device,Please install it manully.in windows You can try to execute" 157 | + System.lineSeparator() 158 | + "adb install " 159 | + System.getProperty("user.home") 160 | + "\\AppData\\Roaming\\npm\\node_modules\\appium\\build\\unlock_apk\\unlock_apk-debug.apk"); 161 | } 162 | innerUnlock(unlockPackage, activity); 163 | } 164 | 165 | private void innerUnlock(String unlockPackage, String activity) { 166 | CommandLine command = adbCommand("shell", "am", "start", "-a", 167 | "android.intent.action.MAIN", "-n", unlockPackage + "/" 168 | + activity); 169 | 170 | String out = executeCommandQuietly(command); 171 | try { 172 | // give it a second to recover from the activity start 173 | Thread.sleep(1000); 174 | } catch (InterruptedException ie) { 175 | log.warn("unlock", ie); 176 | } 177 | log.debug("unlock {}", out); 178 | } 179 | 180 | ; 181 | 182 | @Override 183 | public boolean isInstalled(String appBasePackage) 184 | throws AndroidSdkException { 185 | CommandLine command = adbCommand("shell", "pm", "list", "packages"); 186 | 187 | command.addArgument(appBasePackage, false); 188 | String result = null; 189 | try { 190 | result = ShellCommand.exec(command); 191 | } catch (ShellCommandException e) { 192 | } 193 | 194 | return result != null && result.contains("package:" + appBasePackage); 195 | } 196 | 197 | @Override 198 | public boolean isInstalled(AndroidApp app) { 199 | return isInstalled(app.getBasePackage()); 200 | } 201 | 202 | @Override 203 | public void install(AndroidApp app) { 204 | // Reinstall if already installed, Install otherwise 205 | CommandLine command = adbCommand("install", "-r", app.getAbsolutePath()); 206 | 207 | String out = executeCommandQuietly(command, COMMAND_TIMEOUT * 6); 208 | try { 209 | // give it a second to recover from the install 210 | Thread.sleep(1000); 211 | } catch (InterruptedException ie) { 212 | throw new RuntimeException(ie); 213 | } 214 | if (!out.contains("Success")) { 215 | throw new AndroidSdkException("APK installation failed. Output:\n" 216 | + out); 217 | } 218 | } 219 | 220 | public boolean start(AndroidApp app) { 221 | if (!isInstalled(app)) { 222 | install(app); 223 | } 224 | 225 | String mainActivity = app.getMainActivity().replace( 226 | app.getBasePackage(), ""); 227 | CommandLine command = adbCommand("shell", "am", "start", "-a", 228 | "android.intent.action.MAIN", "-n", app.getBasePackage() + "/" 229 | + mainActivity); 230 | 231 | String out = executeCommandQuietly(command); 232 | try { 233 | // give it a second to recover from the activity start 234 | Thread.sleep(1000); 235 | } catch (InterruptedException ie) { 236 | throw new RuntimeException(ie); 237 | } 238 | return out.contains("Starting: Intent"); 239 | } 240 | 241 | protected String executeCommandQuietly(CommandLine command) { 242 | return executeCommandQuietly(command, COMMAND_TIMEOUT); 243 | } 244 | 245 | protected String executeCommandQuietly(CommandLine command, long timeout) { 246 | try { 247 | return ShellCommand.exec(command, timeout); 248 | } catch (ShellCommandException e) { 249 | String logMessage = String.format("Could not execute command: %s", 250 | command); 251 | log.warn(logMessage, e); 252 | return ""; 253 | } 254 | } 255 | 256 | @Override 257 | public void uninstall(String appBasePackage) { 258 | CommandLine command = adbCommand("uninstall", appBasePackage); 259 | 260 | executeCommandQuietly(command); 261 | try { 262 | // give it a second to recover from the uninstall 263 | Thread.sleep(1000); 264 | } catch (InterruptedException ie) { 265 | throw new RuntimeException(ie); 266 | } 267 | } 268 | 269 | @Override 270 | public void uninstall(AndroidApp app) { 271 | uninstall(app.getBasePackage()); 272 | } 273 | 274 | @Override 275 | public void clearUserData(String appBasePackage) { 276 | CommandLine command = adbCommand("shell", "pm", "clear", appBasePackage); 277 | executeCommandQuietly(command); 278 | } 279 | 280 | @Override 281 | public void clearUserData(AndroidApp app) { 282 | clearUserData(app.getBasePackage()); 283 | } 284 | 285 | @Override 286 | public void kill(String appBasePackage) { 287 | try { 288 | CommandLine command = adbCommand("shell", "am", "force-stop", 289 | appBasePackage); 290 | executeCommandQuietly(command); 291 | } finally { 292 | } 293 | } 294 | 295 | /** 296 | * get current android page's dump file 297 | */ 298 | public String getDump() { 299 | pushAutomator2Device(); 300 | runtest(); 301 | String path = pullDump2PC(); 302 | String xml = ""; 303 | try { 304 | FileInputStream fileInputStream = new FileInputStream(path); 305 | @SuppressWarnings("resource") 306 | BufferedReader in = new BufferedReader( 307 | new InputStreamReader(fileInputStream)); 308 | StringBuffer buffer = new StringBuffer(); 309 | String line = ""; 310 | while ((line = in.readLine()) != null) { 311 | buffer.append(line); 312 | } 313 | xml = buffer.toString(); 314 | } catch (FileNotFoundException e) { 315 | e.printStackTrace(); 316 | } catch (IOException e) { 317 | e.printStackTrace(); 318 | } 319 | return xml; 320 | } 321 | 322 | /** 323 | * try to click GPS Popup window 324 | */ 325 | public boolean handlePopBox(String deviceBrand) { 326 | pushHandleGps2Device(); 327 | CommandLine exeCommand = null; 328 | if (deviceBrand.contains("HTC")) { 329 | 330 | exeCommand = adbCommand("shell", "uiautomator", "runtest", 331 | "/data/local/tmp/handlePopBox.jar", "-c", "com.test.device.gps.HTCGPSTest"); 332 | } else if (deviceBrand.contains("Meizu")) { 333 | 334 | exeCommand = adbCommand("shell", "uiautomator", "runtest", 335 | "/data/local/tmp/handlePopBox.jar", "-c", "com.test.device.gps.MeizuGPSTest"); 336 | } 337 | 338 | String output = executeCommandQuietly(exeCommand); 339 | log.debug("run test {}", output); 340 | 341 | try { 342 | // give it a second to recover from the activity start 343 | Thread.sleep(1000); 344 | } catch (InterruptedException ie) { 345 | throw new RuntimeException(ie); 346 | } 347 | return output.contains("OK"); 348 | } 349 | 350 | /** 351 | * Push handlePopBox.jar to android tmp folder 352 | * 353 | * @return push device successful or not 354 | */ 355 | private boolean pushHandleGps2Device() { 356 | 357 | InputStream io = AbstractDevice.class.getResourceAsStream("handlePopBox.jar"); 358 | File dest = new File(FileUtils.getTempDirectory(), "handlePopBox.jar"); 359 | 360 | try { 361 | FileUtils.copyInputStreamToFile(io, dest); 362 | } catch (IOException e) { 363 | e.printStackTrace(); 364 | } 365 | 366 | CommandLine pushcommand = adbCommand("push ", dest.getAbsolutePath(), "/data/local/tmp/"); 367 | String outputPush = executeCommandQuietly(pushcommand); 368 | log.debug("Push automator.jar to device {}", outputPush); 369 | 370 | try { 371 | // give it a second to recover from the activity start 372 | Thread.sleep(1000); 373 | } catch (InterruptedException ie) { 374 | throw new RuntimeException(ie); 375 | } 376 | return outputPush.contains("KB/s"); 377 | } 378 | 379 | /** 380 | * Push automator.jar to android tmp folder 381 | * 382 | * @return push device successful or not 383 | */ 384 | public boolean pushAutomator2Device() { 385 | InputStream io = AbstractDevice.class.getResourceAsStream("automator.jar"); 386 | File dest = new File(FileUtils.getTempDirectory(), "automator.jar"); 387 | 388 | try { 389 | FileUtils.copyInputStreamToFile(io, dest); 390 | } catch (IOException e) { 391 | e.printStackTrace(); 392 | } 393 | 394 | CommandLine pushcommand = adbCommand("push ", dest.getAbsolutePath(), "/data/local/tmp/"); 395 | String outputPush = executeCommandQuietly(pushcommand); 396 | log.debug("Push automator.jar to device {}", outputPush); 397 | 398 | try { 399 | // give it a second to recover from the activity start 400 | Thread.sleep(1000); 401 | } catch (InterruptedException ie) { 402 | throw new RuntimeException(ie); 403 | } 404 | return outputPush.contains("KB/s"); 405 | } 406 | 407 | /** 408 | * clean file dump.xml, qian.xml, uidump.xml in tmp folder 409 | */ 410 | public void cleanTemp() { 411 | CommandLine dumpcommand = adbCommand("shell", "rm", "-r", 412 | "/data/local/tmp/local/tmp/dump.xml"); 413 | executeCommandQuietly(dumpcommand); 414 | try { 415 | // give it a second to recover from the activity start 416 | Thread.sleep(1000); 417 | } catch (InterruptedException ie) { 418 | throw new RuntimeException(ie); 419 | } 420 | 421 | CommandLine qiancommand = adbCommand("shell", "rm", "-r", 422 | "/data/local/tmp/local/tmp/qian.xml"); 423 | String output = executeCommandQuietly(qiancommand); 424 | log.debug("Delete file qian.xml: {}", output); 425 | try { 426 | // give it a second to recover from the activity start 427 | Thread.sleep(1000); 428 | } catch (InterruptedException ie) { 429 | throw new RuntimeException(ie); 430 | } 431 | 432 | CommandLine command = adbCommand("shell", "rm", "-r", 433 | "/data/local/tmp/uidump.xml"); 434 | executeCommandQuietly(command); 435 | try { 436 | // give it a second to recover from the activity start 437 | Thread.sleep(1000); 438 | } catch (InterruptedException ie) { 439 | throw new RuntimeException(ie); 440 | } 441 | } 442 | 443 | /** 444 | * run command to get dump file 445 | */ 446 | public boolean runtest() { 447 | cleanTemp(); 448 | CommandLine command = adbCommand("shell", "uiautomator", "runtest", 449 | "/data/local/tmp/automator.jar", "-c", "com.uia.example.my.test"); 450 | String output = executeCommandQuietly(command); 451 | log.debug("run test {}", output); 452 | 453 | try { 454 | // give it a second to recover from the activity start 455 | Thread.sleep(1000); 456 | } catch (InterruptedException ie) { 457 | throw new RuntimeException(ie); 458 | } 459 | return output.contains("OK"); 460 | } 461 | 462 | /** 463 | * pull dump file from android device to pc 464 | * 465 | * @return pc dump file path 466 | */ 467 | public String pullDump2PC() { 468 | 469 | String serial = device.getSerialNumber(); 470 | File dest = new File(FileUtils.getTempDirectory(), serial 471 | + ".xml"); 472 | String path = dest.getPath(); 473 | log.debug("pull dump file to pc's path {}", path); 474 | 475 | CommandLine commandpull = adbCommand("pull", "/data/local/tmp/local/tmp/qian.xml", path); 476 | String out = executeCommandQuietly(commandpull); 477 | log.debug("pull dump file to pc's result {}", out); 478 | return path; 479 | } 480 | 481 | @Override 482 | public void kill(AndroidApp aut) { 483 | kill(aut.getBasePackage()); 484 | } 485 | 486 | public void removeForwardPort(int port) { 487 | CommandLine command = adbCommand("forward", "--remove", "tcp:" + port); 488 | try { 489 | ShellCommand.exec(command, COMMAND_TIMEOUT); 490 | } catch (ShellCommandException e) { 491 | log.warn("Could not free Selendroid port", e); 492 | } 493 | } 494 | 495 | public void forwardPort(int local, int remote) { 496 | CommandLine command = adbCommand("forward", "tcp:" + local, "tcp:" 497 | + remote); 498 | try { 499 | ShellCommand.exec(command, COMMAND_TIMEOUT); 500 | } catch (ShellCommandException forwardException) { 501 | String debugForwardList; 502 | try { 503 | debugForwardList = ShellCommand.exec( 504 | adbCommand("forward", "--list"), COMMAND_TIMEOUT); 505 | } catch (ShellCommandException listException) { 506 | debugForwardList = "Could not get list of forwarded ports."; 507 | } 508 | 509 | throw new RuntimeException("Could not forward port: " + command 510 | + "\nList of forwarded ports:\n" + debugForwardList, 511 | forwardException); 512 | } 513 | } 514 | 515 | protected String getProp(String key) { 516 | CommandLine command = adbCommand("shell", "getprop", key); 517 | String prop = executeCommandQuietly(command); 518 | 519 | return prop == null ? "" : prop.replace("\r", "").replace("\n", ""); 520 | } 521 | 522 | protected static String extractValue(String regex, String output) { 523 | Pattern pattern = Pattern.compile(regex, Pattern.MULTILINE); 524 | Matcher matcher = pattern.matcher(output); 525 | if (matcher.find()) { 526 | return matcher.group(1); 527 | } 528 | 529 | return ""; 530 | } 531 | 532 | public String runAdbCommand(String parameter) { 533 | if (parameter == null || parameter.isEmpty()) { 534 | return null; 535 | } 536 | CommandLine command = adbCommand(); 537 | 538 | String[] params = parameter.split(" "); 539 | for (String param : params) { 540 | command.addArgument(param, false); 541 | } 542 | 543 | String commandOutput = executeCommandQuietly(command); 544 | return commandOutput.trim(); 545 | } 546 | 547 | public BufferedImage takeScreenshot() { 548 | if (device == null) { 549 | throw new AndroidDeviceException( 550 | "Device not accessible via ddmlib."); 551 | } 552 | RawImage rawImage; 553 | try { 554 | Profiler profiler = new Profiler("native screen"); 555 | profiler.start("start"); 556 | rawImage = device.getScreenshot(); 557 | 558 | profiler.stop(); 559 | profiler.print(); 560 | } catch (IOException ioe) { 561 | throw new AndroidDeviceException("Unable to get frame buffer: " 562 | + ioe.getMessage()); 563 | } catch (TimeoutException e) { 564 | throw new AndroidDeviceException(e.getMessage()); 565 | } catch (AdbCommandRejectedException e) { 566 | throw new AndroidDeviceException(e.getMessage()); 567 | } 568 | 569 | BufferedImage image = ImageUtils.convertImage(rawImage); 570 | 571 | return image; 572 | } 573 | 574 | @Override 575 | public void takeScreenshot(String fileUrl) { 576 | BufferedImage image = takeScreenshot(); 577 | ImageUtils.writeToFile(image, fileUrl); 578 | } 579 | 580 | /** 581 | * Use adb to send a keyevent to the device.

Full list of keys available here: 582 | * http://developer.android.com/reference/android/view/KeyEvent.html 583 | * 584 | * @param value - Key to be sent to 'adb shell input keyevent' 585 | */ 586 | public void inputKeyevent(int value) { 587 | executeCommandQuietly(adbCommand("shell", "input", "keyevent", "" 588 | + value)); 589 | // need to wait a beat for the UI to respond 590 | try { 591 | Thread.sleep(500); 592 | } catch (InterruptedException e) { 593 | log.warn("", e); 594 | } 595 | } 596 | 597 | public void invokeActivity(String activity) { 598 | executeCommandQuietly(adbCommand("shell", "am", "start", "-a", activity)); 599 | // need to wait a beat for the UI to respond 600 | try { 601 | Thread.sleep(500); 602 | } catch (InterruptedException e) { 603 | log.warn("", e); 604 | } 605 | } 606 | 607 | public void restartADB() { 608 | executeCommandQuietly(adbCommand("kill-server")); 609 | try { 610 | Thread.sleep(500); 611 | } catch (InterruptedException e) { 612 | log.warn("", e); 613 | } 614 | // make sure it's backup again 615 | executeCommandQuietly(adbCommand("devices")); 616 | } 617 | 618 | private CommandLine adbCommand() { 619 | CommandLine command = new CommandLine(AndroidSdk.adb()); 620 | if (isSerialConfigured()) { 621 | command.addArgument("-s", false); 622 | command.addArgument(serial, false); 623 | } 624 | return command; 625 | } 626 | 627 | private CommandLine adbCommand(String... args) { 628 | CommandLine command = adbCommand(); 629 | for (String arg : args) { 630 | command.addArgument(arg, false); 631 | } 632 | return command; 633 | } 634 | 635 | public String getExternalStoragePath() { 636 | return runAdbCommand("shell echo $EXTERNAL_STORAGE"); 637 | } 638 | 639 | /** 640 | * Get crash log from AUT 641 | * 642 | * @return empty string if there is no crash log on the device, otherwise returns the stack trace 643 | * caused by the crash of the AUT 644 | */ 645 | public String getCrashLog() { 646 | String crashLogFileName = null; 647 | File crashLogFile = new File(getExternalStoragePath(), crashLogFileName); 648 | 649 | // the "test" utility doesn't exist on all devices so we'll check the 650 | // output of ls. 651 | CommandLine directoryListCommand = adbCommand("shell", "ls", 652 | crashLogFile.getParentFile().getAbsolutePath()); 653 | String directoryList = executeCommandQuietly(directoryListCommand); 654 | if (directoryList.contains(crashLogFileName)) { 655 | return executeCommandQuietly(adbCommand("shell", "cat", 656 | crashLogFile.getAbsolutePath())); 657 | } 658 | 659 | return ""; 660 | } 661 | 662 | @Override 663 | public boolean equals(Object o) { 664 | if (this == o) { 665 | return true; 666 | } 667 | if (o == null || getClass() != o.getClass() || device == null) { 668 | return false; 669 | } 670 | 671 | AbstractDevice that = (AbstractDevice) o; 672 | 673 | return device.equals(that.device); 674 | } 675 | 676 | @Override 677 | public void tap(int x, int y) { 678 | CommandLine command = adbCommand("shell", "input", "tap", 679 | String.valueOf(x), String.valueOf(y)); 680 | 681 | executeCommandQuietly(command, COMMAND_TIMEOUT * 6); 682 | try { 683 | Thread.sleep(1000); 684 | } catch (InterruptedException ie) { 685 | throw new RuntimeException(ie); 686 | } 687 | } 688 | 689 | ; 690 | 691 | @Override 692 | public void swipe(int x1, int y1, int x2, int y2) { 693 | CommandLine command = adbCommand("shell", "input", "swipe", 694 | String.valueOf(x1), String.valueOf(y1), String.valueOf(x2), 695 | String.valueOf(y2)); 696 | 697 | executeCommandQuietly(command, COMMAND_TIMEOUT * 6); 698 | try { 699 | Thread.sleep(1000); 700 | } catch (InterruptedException ie) { 701 | throw new RuntimeException(ie); 702 | } 703 | } 704 | 705 | @Override 706 | public boolean isWifiOff() { 707 | CommandLine command = adbCommand("shell", "settings", "get", "global", 708 | "wifi_on"); 709 | String commandOutput = executeCommandQuietly(command); 710 | String result = commandOutput.trim(); 711 | 712 | return "1".equals(result) ? false : true; 713 | } 714 | 715 | @Override 716 | public String getName() { 717 | return device.getName(); 718 | } 719 | 720 | @Override 721 | public AndroidDeviceBrand getBrand() { 722 | if (brand != null) { 723 | return brand; 724 | } 725 | String name = getName(); 726 | String manufacture = StringUtils.substringBefore(name, "-"); 727 | String model = StringUtils.substringBetween(name, "-", "-"); 728 | brand = AndroidDeviceBrand.from(manufacture, model); 729 | return brand; 730 | } 731 | 732 | ; 733 | 734 | @Override 735 | public DeviceInfo getDeviceInfo() { 736 | 737 | DeviceInfo deviceInfo = new DeviceInfo(); 738 | deviceInfo.setName(this.getName()); 739 | deviceInfo.setSerial(this.getSerialNumber()); 740 | deviceInfo.setDensity(device.getDensity()); 741 | DeviceTargetPlatform tf = this.getTargetPlatform(); 742 | deviceInfo.setOsName(tf.formatedName()); 743 | 744 | try { 745 | deviceInfo.setKernel(device.getSystemProperty("ro.build.kernel.id") 746 | .get()); 747 | deviceInfo.setBattery(device.getBattery().get()); 748 | } catch (InterruptedException | ExecutionException e) { 749 | log.warn("getDeviceInfo", e); 750 | } 751 | 752 | return deviceInfo; 753 | } 754 | 755 | ; 756 | 757 | private LogCatReceiverTask logCatReceiverTask; 758 | private final Set logCatListeners = new HashSet<>(); 759 | ; 760 | private boolean logCatRunning = false; 761 | 762 | @Override 763 | public synchronized void addLogCatListener(LogCatListener logCatListener) { 764 | if (logCatReceiverTask == null) { 765 | logCatReceiverTask = new LogCatReceiverTask(device); 766 | } 767 | logCatReceiverTask.addLogCatListener(logCatListener); 768 | logCatListeners.add(logCatListener); 769 | if (!logCatRunning) { 770 | logCatReceiverTask.run(); 771 | logCatRunning = true; 772 | } 773 | } 774 | 775 | @Override 776 | public synchronized void removeLogCatListener(LogCatListener logCatListener) { 777 | if (logCatReceiverTask == null) { 778 | logCatReceiverTask = new LogCatReceiverTask(device); 779 | } 780 | logCatReceiverTask.removeLogCatListener(logCatListener); 781 | logCatListeners.remove(logCatListener); 782 | if (logCatListeners.size() < 1) { 783 | logCatReceiverTask.stop(); 784 | logCatRunning = false; 785 | } 786 | } 787 | 788 | @Override 789 | public List getClientDatasInfo() { 790 | List dataInfos = new ArrayList(); 791 | Client clients[] = this.device.getClients(); 792 | for (int i = 0; i < clients.length; i++) { 793 | dataInfos.add(new ClientDataInfo(clients[i])); 794 | } 795 | return dataInfos; 796 | } 797 | 798 | @Override 799 | public Client[] getAllClient() { 800 | return this.device.getClients(); 801 | } 802 | 803 | @Override 804 | public Client getClientByAppName(String appName) { 805 | return this.device.getClient(appName); 806 | } 807 | 808 | @Override 809 | public int hashCode() { 810 | return device.hashCode(); 811 | } 812 | } 813 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/impl/AndroidDeviceStore.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.impl; 2 | 3 | import com.android.ddmlib.AndroidDebugBridge; 4 | import com.android.ddmlib.DdmPreferences; 5 | import com.android.ddmlib.IDevice; 6 | import com.github.cosysoft.device.DeviceStore; 7 | import com.github.cosysoft.device.android.AndroidDevice; 8 | import com.github.cosysoft.device.exception.AndroidDeviceException; 9 | import com.github.cosysoft.device.exception.DeviceNotFoundException; 10 | import com.github.cosysoft.device.exception.NestedException; 11 | import com.github.cosysoft.device.shell.AndroidSdk; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.TreeSet; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | public class AndroidDeviceStore implements DeviceStore { 19 | 20 | protected static final Logger logger = LoggerFactory 21 | .getLogger(AndroidDeviceStore.class); 22 | 23 | private Map connectedDevices = new HashMap(); 24 | 25 | private AndroidDebugBridge bridge; 26 | private boolean shouldKeepAdbAlive = false; 27 | 28 | static class DeviceStoreHolder { 29 | 30 | static final AndroidDeviceStore instance = init(); 31 | 32 | static AndroidDeviceStore init() { 33 | AndroidDeviceStore instance; 34 | instance = new AndroidDeviceStore(); 35 | instance.initAndroidDevices(false); 36 | return instance; 37 | } 38 | } 39 | 40 | public static AndroidDeviceStore getInstance() { 41 | return DeviceStoreHolder.instance; 42 | } 43 | 44 | /** 45 | * call once 46 | */ 47 | public void initAndroidDevices(boolean shouldKeepAdbAlive) 48 | throws AndroidDeviceException { 49 | // DdmPreferences.setLogLevel(LogLevel.VERBOSE.getStringValue()); 50 | DdmPreferences.setInitialThreadUpdate(true); 51 | DdmPreferences.setInitialHeapUpdate(true); 52 | this.initializeAdbConnection(); 53 | } 54 | 55 | /** 56 | * Initializes the AndroidDebugBridge and registers the DefaultHardwareDeviceManager with the 57 | * AndroidDebugBridge device change listener. 58 | */ 59 | protected void initializeAdbConnection() { 60 | // Get a device bridge instance. Initialize, create and restart. 61 | try { 62 | AndroidDebugBridge.init(true); 63 | } catch (IllegalStateException e) { 64 | // When we keep the adb connection alive the AndroidDebugBridge may 65 | // have been already 66 | // initialized at this point and it generates an exception. Do not 67 | // print it. 68 | if (!shouldKeepAdbAlive) { 69 | logger.error( 70 | "The IllegalStateException is not a show " 71 | + "stopper. It has been handled. This is just debug spew. Please proceed.", 72 | e); 73 | throw new NestedException("ADB init failed", e); 74 | } 75 | } 76 | 77 | bridge = AndroidDebugBridge.getBridge(); 78 | 79 | if (bridge == null) { 80 | bridge = AndroidDebugBridge.createBridge(AndroidSdk.adb() 81 | .getAbsolutePath(), false); 82 | } 83 | 84 | long timeout = System.currentTimeMillis() + 60000; 85 | while (!bridge.hasInitialDeviceList() 86 | && System.currentTimeMillis() < timeout) { 87 | try { 88 | Thread.sleep(50); 89 | } catch (InterruptedException e) { 90 | throw new RuntimeException(e); 91 | } 92 | } 93 | // Add the existing devices to the list of devices we are tracking. 94 | IDevice[] devices = bridge.getDevices(); 95 | logger.info("initialDeviceList size {}", devices.length); 96 | for (int i = 0; i < devices.length; i++) { 97 | logger.info("devices state: {},{} ", devices[i].getName(), 98 | devices[i].getState()); 99 | connectedDevices.put(devices[i], new DefaultHardwareDevice( 100 | devices[i])); 101 | } 102 | 103 | bridge.addDeviceChangeListener(new DeviceChangeListener(connectedDevices)); 104 | } 105 | 106 | @Override 107 | public TreeSet getDevices() { 108 | return new TreeSet(connectedDevices.values()); 109 | } 110 | 111 | @Override 112 | public AndroidDevice getDeviceBySerial(String serialID) { 113 | for (AndroidDevice device : getDevices()) { 114 | if (device.getSerialNumber().equalsIgnoreCase(serialID)) { 115 | return device; 116 | } 117 | } 118 | throw new DeviceNotFoundException(String.format("Device %s not found", serialID)); 119 | } 120 | 121 | /** 122 | * Shutdown the AndroidDebugBridge and clean up all connected devices. 123 | */ 124 | @Override 125 | public void shutdown() { 126 | if (!shouldKeepAdbAlive) { 127 | AndroidDebugBridge.disconnectBridge(); 128 | AndroidDebugBridge.terminate(); 129 | } 130 | logger.info("stopping Device Manager"); 131 | } 132 | 133 | /** 134 | * used with caution or don't call this method 135 | */ 136 | @Override 137 | public void shutdownForcely() { 138 | AndroidDebugBridge.disconnectBridge(); 139 | AndroidDebugBridge.terminate(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/impl/DefaultAndroidApp.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.impl; 2 | 3 | import com.github.cosysoft.device.android.AndroidApp; 4 | import com.github.cosysoft.device.shell.AndroidSdkException; 5 | import com.github.cosysoft.device.shell.ShellCommandException; 6 | import org.apache.commons.exec.CommandLine; 7 | import com.github.cosysoft.device.model.AppInfo; 8 | import com.github.cosysoft.device.shell.AndroidSdk; 9 | import com.github.cosysoft.device.shell.ShellCommand; 10 | 11 | import java.io.File; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | 15 | public class DefaultAndroidApp implements AndroidApp { 16 | private File apkFile; 17 | private String mainPackage = null; 18 | protected String mainActivity = null; 19 | private String versionName = null; 20 | 21 | public DefaultAndroidApp(File apkFile) { 22 | this.apkFile = apkFile; 23 | } 24 | 25 | private String extractApkDetails(String regex) 26 | throws ShellCommandException, AndroidSdkException { 27 | CommandLine line = new CommandLine(AndroidSdk.aapt()); 28 | 29 | line.addArgument("dump", false); 30 | line.addArgument("badging", false); 31 | line.addArgument(apkFile.getAbsolutePath(), false); 32 | String output=""; 33 | try { 34 | output = ShellCommand.exec(line, 20000); 35 | } catch (Exception e) { 36 | output=e.getCause().getMessage(); 37 | } 38 | 39 | Pattern pattern = Pattern.compile(regex); 40 | Matcher matcher = pattern.matcher(output); 41 | if (matcher.find()) { 42 | return matcher.group(1); 43 | } 44 | 45 | return null; 46 | } 47 | 48 | @Override 49 | public String getBasePackage() throws AndroidSdkException { 50 | if (mainPackage == null) { 51 | try { 52 | mainPackage = extractApkDetails("package: name='(.*?)'"); 53 | } catch (ShellCommandException e) { 54 | 55 | 56 | throw new RuntimeException("The base package name of the apk " 57 | + apkFile.getName() + " cannot be extracted."); 58 | } 59 | 60 | } 61 | return mainPackage; 62 | } 63 | 64 | @Override 65 | public String getMainActivity() throws AndroidSdkException { 66 | if (mainActivity == null) { 67 | try { 68 | mainActivity = extractApkDetails("launchable-activity: name='(.*?)'"); 69 | } catch (ShellCommandException e) { 70 | throw new RuntimeException("The main activity of the apk " 71 | + apkFile.getName() + " cannot be extracted."); 72 | } 73 | } 74 | return mainActivity; 75 | } 76 | 77 | public void setMainActivity(String mainActivity) { 78 | this.mainActivity = mainActivity; 79 | } 80 | 81 | @Override 82 | public void deleteFileFromWithinApk(String file) 83 | throws ShellCommandException, AndroidSdkException { 84 | CommandLine line = new CommandLine(AndroidSdk.aapt()); 85 | line.addArgument("remove", false); 86 | line.addArgument(apkFile.getAbsolutePath(), false); 87 | line.addArgument(file, false); 88 | 89 | ShellCommand.exec(line, 20000); 90 | } 91 | 92 | @Override 93 | public String getAbsolutePath() { 94 | return apkFile.getAbsolutePath(); 95 | } 96 | 97 | @Override 98 | public String getVersionName() throws AndroidSdkException { 99 | if (versionName == null) { 100 | try { 101 | versionName = extractApkDetails("versionName='(.*?)'"); 102 | } catch (ShellCommandException e) { 103 | throw new RuntimeException("The versionName of the apk " 104 | + apkFile.getName() + " cannot be extracted."); 105 | } 106 | } 107 | return versionName; 108 | } 109 | 110 | public String getAppId() throws AndroidSdkException { 111 | return getBasePackage() + ":" + getVersionName(); 112 | } 113 | 114 | public AppInfo toAppInfo() { 115 | 116 | AppInfo appInfo = new AppInfo(); 117 | appInfo.setMainActivity(this.getMainActivity()); 118 | appInfo.setPackageURI(this.getAbsolutePath()); 119 | appInfo.setBasePackage(this.getBasePackage()); 120 | appInfo.setVersion(this.getVersionName()); 121 | 122 | return appInfo; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/impl/DefaultHardwareDevice.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.impl; 2 | 3 | import com.android.ddmlib.IDevice; 4 | import com.android.ddmlib.RawImage; 5 | import com.github.cosysoft.device.android.DeviceTargetPlatform; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.awt.*; 10 | import java.util.Locale; 11 | 12 | /** 13 | * reviewed 14 | * 15 | * 16 | */ 17 | public class DefaultHardwareDevice extends AbstractDevice implements 18 | Comparable { 19 | 20 | private static final Logger log = LoggerFactory 21 | .getLogger(DefaultHardwareDevice.class); 22 | 23 | private Locale locale = null; 24 | private DeviceTargetPlatform targetPlatform = null; 25 | private Dimension screenSize = null; 26 | 27 | public DefaultHardwareDevice(IDevice device) { 28 | super(device); 29 | } 30 | 31 | @Override 32 | protected String getProp(String key) { 33 | return device.getProperty(key); 34 | } 35 | 36 | @Override 37 | public DeviceTargetPlatform getTargetPlatform() { 38 | if (targetPlatform == null) { 39 | String version = getProp("ro.build.version.sdk"); 40 | targetPlatform = DeviceTargetPlatform.fromInt(version); 41 | } 42 | return targetPlatform; 43 | } 44 | 45 | @Override 46 | public Dimension getScreenSize() { 47 | if (this.screenSize == null) { 48 | try { 49 | RawImage screenshot = device.getScreenshot(); 50 | this.screenSize = new Dimension(screenshot.width, 51 | screenshot.height); 52 | } catch (Exception e) { 53 | log.warn("was not able to determine screensize: " 54 | + e.getMessage()); 55 | } 56 | } 57 | 58 | return this.screenSize; 59 | } 60 | 61 | @Override 62 | public Locale getLocale() { 63 | if (this.locale == null) { 64 | String language = getProp("persist.sys.language"); 65 | String country = getProp("persist.sys.country"); 66 | if (language != null && country != null) { 67 | this.locale = new Locale(language, country); 68 | } 69 | 70 | } 71 | return locale; 72 | } 73 | 74 | @Override 75 | public boolean isDeviceReady() { 76 | return true; 77 | } 78 | 79 | @Override 80 | public String getSerialNumber() { 81 | return serial; 82 | } 83 | 84 | @Override 85 | public String toString() { 86 | return "DefaultHardwareDevice [brand=" + getBrand() + ", locale=" 87 | + getLocale() + ", targetVersion=" + getTargetPlatform() 88 | + ", getName()=" + getName() + "]"; 89 | } 90 | 91 | @Override 92 | public int compareTo(AbstractDevice o) { 93 | return this.serial.compareTo(o.serial); 94 | } 95 | 96 | @Override 97 | public int hashCode() { 98 | final int prime = 31; 99 | int result = super.hashCode(); 100 | result = prime * result + ((serial == null) ? 0 : serial.hashCode()); 101 | return result; 102 | } 103 | 104 | @Override 105 | public boolean equals(Object obj) { 106 | if (this == obj) 107 | return true; 108 | if (!super.equals(obj)) 109 | return false; 110 | if (getClass() != obj.getClass()) 111 | return false; 112 | DefaultHardwareDevice other = (DefaultHardwareDevice) obj; 113 | if (serial == null) { 114 | if (other.serial != null) 115 | return false; 116 | } else if (!serial.equals(other.serial)) 117 | return false; 118 | return true; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/impl/DeviceChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.impl; 2 | 3 | import com.android.ddmlib.AndroidDebugBridge; 4 | import com.android.ddmlib.IDevice; 5 | import com.github.cosysoft.device.android.AndroidDevice; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.util.Iterator; 10 | import java.util.Map; 11 | 12 | /** 13 | * 14 | */ 15 | public class DeviceChangeListener implements AndroidDebugBridge.IDeviceChangeListener { 16 | 17 | private static final Logger logger = LoggerFactory 18 | .getLogger(DeviceChangeListener.class); 19 | private final Map connectedDevices; 20 | 21 | public DeviceChangeListener(Map connectedDevices) { 22 | this.connectedDevices = connectedDevices; 23 | } 24 | 25 | @Override 26 | public void deviceConnected(IDevice device) { 27 | logger.info("deviceConnected {}", device.getSerialNumber()); 28 | AndroidDevice ad = new DefaultHardwareDevice(device); 29 | Iterator> entryIterator = connectedDevices.entrySet().iterator(); 30 | boolean contain = false; 31 | while (entryIterator.hasNext()) { 32 | Map.Entry entry = entryIterator.next(); 33 | if (entry.getValue().equals(ad)) { 34 | contain = true; 35 | break; 36 | } 37 | } 38 | if (!contain) { 39 | connectedDevices.put(device, ad); 40 | } 41 | } 42 | 43 | @Override 44 | public void deviceDisconnected(IDevice device) { 45 | logger.info("deviceDisconnected {}", device.getSerialNumber()); 46 | AndroidDevice ad = new DefaultHardwareDevice(device); 47 | Iterator> entryIterator = connectedDevices.entrySet().iterator(); 48 | while (entryIterator.hasNext()) { 49 | Map.Entry entry = entryIterator.next(); 50 | if (entry.getValue().equals(ad)) { 51 | entryIterator.remove(); 52 | } 53 | } 54 | } 55 | 56 | @Override 57 | public void deviceChanged(IDevice device, int changeMask) { 58 | logger.info(device.getSerialNumber() + " " + changeMask); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/impl/InstalledAndroidApp.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.impl; 2 | 3 | import com.github.cosysoft.device.android.AndroidApp; 4 | 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | public class InstalledAndroidApp implements AndroidApp { 9 | 10 | private String packageName; 11 | private String activityName; 12 | private String version; 13 | 14 | public InstalledAndroidApp(String appInfo) { 15 | Pattern infoPattern = Pattern.compile("(.+):(.+)/(.+)"); 16 | Matcher patternMatcher = infoPattern.matcher(appInfo); 17 | if (patternMatcher.matches()) { 18 | packageName = patternMatcher.group(1); 19 | version = patternMatcher.group(2); 20 | activityName = patternMatcher.group(3); 21 | } else { 22 | throw new RuntimeException( 23 | "Format for installed app is: tld.company.app:version/ActivityClass"); 24 | } 25 | } 26 | 27 | @Override 28 | public String getBasePackage() { 29 | return packageName; 30 | } 31 | 32 | @Override 33 | public String getMainActivity() { 34 | return (activityName.contains(".")) ? activityName : packageName + "." 35 | + activityName; 36 | } 37 | 38 | public void setMainActivity(String mainActivity) { 39 | this.activityName = mainActivity; 40 | } 41 | 42 | @Override 43 | public String getVersionName() { 44 | return version; 45 | } 46 | 47 | @Override 48 | public void deleteFileFromWithinApk(String file) { 49 | // no-op 50 | } 51 | 52 | @Override 53 | public String getAppId() { 54 | return packageName + ":" + version; 55 | } 56 | 57 | @Override 58 | public String getAbsolutePath() { 59 | return null; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/xiaomi/MIDeviceUtility.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.xiaomi; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.concurrent.Callable; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.Future; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | import org.apache.commons.io.FileUtils; 13 | import org.apache.commons.lang3.StringUtils; 14 | import com.github.cosysoft.device.android.AndroidApp; 15 | import com.github.cosysoft.device.android.AndroidDevice; 16 | import com.github.cosysoft.device.android.AndroidDeviceBrand; 17 | import com.github.cosysoft.device.android.impl.DefaultAndroidApp; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | /** 22 | * fix xiaomi3 device installer popup problem 23 | * 24 | * @author ltyao 25 | * 26 | */ 27 | public class MIDeviceUtility { 28 | 29 | private static final Logger logger = LoggerFactory 30 | .getLogger(MIDeviceUtility.class); 31 | 32 | public static boolean install(AndroidDevice device, AndroidApp app) { 33 | 34 | boolean result = false; 35 | ExecutorService installExecutor = Executors.newFixedThreadPool(2); 36 | MIDevice miDevice = new MIDevice(device); 37 | InstallHelperTask helper = new InstallHelperTask(miDevice); 38 | InstallTask installTask = new InstallTask(miDevice, app); 39 | 40 | Future rs = installExecutor.submit(installTask); 41 | installExecutor.submit(helper); 42 | installExecutor.shutdown(); 43 | 44 | try { 45 | Boolean rsw = rs.get(60000, TimeUnit.MILLISECONDS); 46 | logger.info("iresult=" + rsw); 47 | result = device.isInstalled(app); 48 | 49 | } catch (Exception e) { 50 | logger.warn("xiaomi install ", e); 51 | } finally { 52 | installExecutor.shutdownNow(); 53 | } 54 | logger.info("Xiaomi Installer result={}", result); 55 | return result; 56 | } 57 | 58 | public static boolean testInstall(AndroidDevice device) { 59 | 60 | boolean result = false; 61 | InputStream io = MIDeviceUtility.class 62 | .getResourceAsStream("CapMI_1.0.apk"); 63 | String serial = device.getSerialNumber(); 64 | File dest = new File(FileUtils.getTempDirectory(), serial + ".apk"); 65 | try { 66 | FileUtils.copyInputStreamToFile(io, dest); 67 | AndroidApp app = new DefaultAndroidApp(dest); 68 | device.unlock(); 69 | device.uninstall(app); 70 | result = install(device, app); 71 | 72 | } catch (IOException e) { 73 | logger.warn("mi test install ", e); 74 | } 75 | return result; 76 | 77 | } 78 | 79 | public static class MIDevice { 80 | 81 | public AndroidDevice getDevice() { 82 | return device; 83 | } 84 | 85 | private AndroidDevice device; 86 | private AndroidDeviceBrand brand = null; 87 | 88 | public MIDevice(AndroidDevice device) { 89 | this.device = device; 90 | this.brand = device.getBrand(); 91 | } 92 | 93 | public void unlock() { 94 | device.unlock(); 95 | } 96 | 97 | public void crossConfirmView() { 98 | if (AndroidDeviceBrand.XIAOMI_MI_3.equals(brand) 99 | || AndroidDeviceBrand.XIAOMI_MI_3W.equals(brand)) { 100 | device.tap(780, 1800); 101 | } else if (AndroidDeviceBrand.XIAOMI_MI_2.equals(brand)) { 102 | device.tap(510, 1190); 103 | } 104 | 105 | } 106 | 107 | public boolean isConfirm() { 108 | String activity = device.currentActivity(); 109 | return StringUtils.containsIgnoreCase(activity, 110 | "PackageInstallerActivity"); 111 | 112 | } 113 | 114 | } 115 | 116 | static class InstallTask implements Callable { 117 | 118 | MIDevice miDevice; 119 | AndroidApp app; 120 | 121 | public InstallTask(MIDevice miDevice, AndroidApp app) { 122 | super(); 123 | this.miDevice = miDevice; 124 | this.app = app; 125 | } 126 | 127 | public InstallTask(AndroidApp app) { 128 | this.app = app; 129 | } 130 | 131 | @Override 132 | public Boolean call() throws Exception { 133 | miDevice.getDevice().unlock(); 134 | miDevice.getDevice().install(app); 135 | return true; 136 | 137 | } 138 | 139 | } 140 | 141 | static class InstallHelperTask implements Callable { 142 | 143 | protected volatile boolean finished = false; 144 | MIDevice miDevice; 145 | 146 | public InstallHelperTask(MIDevice miDevice) { 147 | super(); 148 | this.miDevice = miDevice; 149 | } 150 | 151 | @Override 152 | public Void call() throws Exception { 153 | try { 154 | 155 | long milliseconds = 10000; 156 | long start = System.currentTimeMillis(); 157 | int count = 0; 158 | while ((System.currentTimeMillis() - start) < milliseconds) { 159 | Thread.sleep(500); 160 | if (miDevice.isConfirm() && count++ > 1) { 161 | logger.debug("Confirm"); 162 | miDevice.crossConfirmView(); 163 | break; 164 | } else { 165 | logger.debug("Wait PackageInstaller Activity"); 166 | } 167 | } 168 | 169 | } catch (Exception e) { 170 | // logger.info("InstallHelperTask Timeout", e); 171 | } finally { 172 | logger.debug("finished"); 173 | finished = true; 174 | } 175 | return null; 176 | } 177 | 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/xiaomi/MIInstaller.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.xiaomi; 2 | 3 | import com.github.cosysoft.device.android.AndroidApp; 4 | import com.github.cosysoft.device.android.AndroidDevice; 5 | import com.github.cosysoft.device.android.impl.AndroidDeviceStore; 6 | import com.github.cosysoft.device.android.impl.DefaultAndroidApp; 7 | import com.github.cosysoft.device.exception.DeviceNotFoundException; 8 | import java.io.File; 9 | import java.util.Set; 10 | import org.apache.commons.cli.CommandLine; 11 | import org.apache.commons.cli.CommandLineParser; 12 | import org.apache.commons.cli.GnuParser; 13 | import org.apache.commons.cli.HelpFormatter; 14 | import org.apache.commons.cli.OptionBuilder; 15 | import org.apache.commons.cli.Options; 16 | import org.apache.commons.cli.ParseException; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | /** 21 | * for Avatar or other non Java Platform 22 | * 23 | * @author ltyao 24 | * 25 | */ 26 | public class MIInstaller { 27 | 28 | private static Logger logger = LoggerFactory.getLogger(MIInstaller.class); 29 | 30 | @SuppressWarnings("static-access") 31 | public static void main(String[] args) throws ParseException { 32 | CommandLineParser parser = new GnuParser(); 33 | 34 | Options options = new Options(); 35 | 36 | options.addOption("i", "install", true, "Install App"); 37 | options.addOption("h", "help", false, "Help"); 38 | 39 | options.addOption(OptionBuilder.withArgName("udid").withLongOpt("udid") 40 | .withDescription("udid").hasArg().create()); 41 | 42 | CommandLine line = parser.parse(options, args); 43 | 44 | // java -jar cap-device-1.0.0-SNAPSHOT-MIInstaller.jar --udid 45 | // HC46FWY03303 -i d 46 | if (args.length == 0 || line.hasOption("h")) { 47 | HelpFormatter formatter = new HelpFormatter(); 48 | formatter.printHelp("help", options); 49 | return; 50 | } 51 | 52 | String install = line.getOptionValue("i"); 53 | String udid = line.getOptionValue("udid"); 54 | 55 | logger.info(String.format( 56 | "with arguments:\n install package %s\n udid %s ", install, 57 | udid)); 58 | 59 | if (install == null) { 60 | logger.info("app package path needed!"); 61 | return; 62 | } 63 | File pkg = new File(install); 64 | if (!pkg.exists()) { 65 | logger.info("app package path is not exist"); 66 | return; 67 | } 68 | install(udid, install); 69 | 70 | } 71 | 72 | private static void install(String udid, String iPackage) { 73 | 74 | AndroidDevice device = getDevice(udid); 75 | 76 | try { 77 | MIDeviceUtility.testInstall(device); 78 | } catch (Exception e) { 79 | logger.error("", e); 80 | } 81 | try { 82 | AndroidApp app = new DefaultAndroidApp(new File(iPackage)); 83 | if (device.isInstalled(app)) { 84 | device.uninstall(app); 85 | } 86 | device.install(app); 87 | } catch (Exception e) { 88 | logger.error("", e); 89 | } finally { 90 | AndroidDeviceStore.getInstance().shutdownForcely(); 91 | } 92 | 93 | } 94 | 95 | private static AndroidDevice getDevice(String udid) { 96 | 97 | Set devices = AndroidDeviceStore.getInstance() 98 | .getDevices(); 99 | for (AndroidDevice androidDevice : devices) { 100 | if (androidDevice.getSerialNumber().equals(udid)) { 101 | return androidDevice; 102 | } 103 | } 104 | throw new DeviceNotFoundException(String.format("udid %s not founded", 105 | udid)); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/android/xiaomi/MIJuger.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.xiaomi; 2 | /*package com.ctrip.cap.device.android.xiaomi; 3 | 4 | import java.awt.image.BufferedImage; 5 | import java.io.File; 6 | import java.io.IOException; 7 | 8 | import javax.imageio.ImageIO; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import com.ctrip.cap.device.image.ImageUtils; 14 | 15 | *//** 16 | * 17 | * @author ltyao 18 | * 19 | *//* 20 | public interface MIJuger { 21 | 22 | public static final int iHeight = 300; 23 | 24 | boolean isConfirm(BufferedImage image); 25 | 26 | public static class MI3Juger implements MIJuger { 27 | 28 | private static final Logger logger = LoggerFactory 29 | .getLogger(MI3Juger.class); 30 | 31 | private static BufferedImage CONFIRM_IMAGE = null; 32 | private static BufferedImage LOCKED_IMAGE = null; 33 | 34 | static { 35 | try { 36 | CONFIRM_IMAGE = ImageIO.read(MIJuger.class 37 | .getResourceAsStream("m3/isure.png")); 38 | LOCKED_IMAGE = ImageIO.read(MIJuger.class 39 | .getResourceAsStream("m3/lock.png")); 40 | } catch (IOException e) { 41 | logger.error("", e); 42 | } 43 | } 44 | 45 | public boolean isConfirm(BufferedImage image) { 46 | BufferedImage sub = image.getSubimage(0, image.getHeight() 47 | - iHeight, image.getWidth(), iHeight); 48 | toTmpFile(sub); 49 | return ImageUtils.sameAs(sub, CONFIRM_IMAGE, 0.8); 50 | } 51 | 52 | public boolean isLocked(BufferedImage image) { 53 | return ImageUtils.sameAs(image, LOCKED_IMAGE, 0.8); 54 | } 55 | 56 | private static void toTmpFile(BufferedImage sub) { 57 | try { 58 | File tmp = File.createTempFile("xiaomi-sub", ".png"); 59 | ImageIO.write(sub, "png", tmp); 60 | } catch (IOException e) { 61 | logger.warn("toTempFile", e); 62 | } 63 | } 64 | 65 | } 66 | 67 | public class MI3WJuger implements MIJuger { 68 | 69 | private static final Logger logger = LoggerFactory 70 | .getLogger(MI3WJuger.class); 71 | 72 | private static BufferedImage CONFIRM_IMAGE = null; 73 | private static BufferedImage LOCKED_IMAGE = null; 74 | 75 | static { 76 | try { 77 | CONFIRM_IMAGE = ImageIO.read(MIJuger.class 78 | .getResourceAsStream("m3/isure.png")); 79 | LOCKED_IMAGE = ImageIO.read(MIJuger.class 80 | .getResourceAsStream("m3/lock.png")); 81 | } catch (IOException e) { 82 | logger.error("", e); 83 | } 84 | } 85 | 86 | public boolean isConfirm(BufferedImage image) { 87 | BufferedImage sub = image.getSubimage(0, image.getHeight() 88 | - iHeight, image.getWidth(), iHeight); 89 | toTmpFile(sub); 90 | return ImageUtils.sameAs(sub, CONFIRM_IMAGE, 0.8); 91 | } 92 | 93 | public boolean isLocked(BufferedImage image) { 94 | return ImageUtils.sameAs(image, LOCKED_IMAGE, 0.8); 95 | } 96 | 97 | private static void toTmpFile(BufferedImage sub) { 98 | try { 99 | File tmp = File.createTempFile("xiaomi-sub", ".png"); 100 | ImageIO.write(sub, "png", tmp); 101 | } catch (IOException e) { 102 | logger.warn("toTempFile", e); 103 | } 104 | } 105 | 106 | } 107 | 108 | public class MI2Juger implements MIJuger { 109 | private static final Logger logger = LoggerFactory 110 | .getLogger(MI3Juger.class); 111 | 112 | private static BufferedImage M2_CONFIRM_IMAGE = null; 113 | private static BufferedImage M2_LOCKED_IMAGE = null; 114 | 115 | static { 116 | try { 117 | M2_CONFIRM_IMAGE = ImageIO.read(MIJuger.class 118 | .getResourceAsStream("m2/isure.png")); 119 | M2_LOCKED_IMAGE = ImageIO.read(MIJuger.class 120 | .getResourceAsStream("m2/lock.png")); 121 | } catch (IOException e) { 122 | logger.error("", e); 123 | } 124 | } 125 | 126 | @Override 127 | public boolean isConfirm(BufferedImage image) { 128 | 129 | BufferedImage sub = image.getSubimage(0, image.getHeight() 130 | - iHeight, image.getWidth(), iHeight); 131 | 132 | return ImageUtils.sameAs(sub, M2_CONFIRM_IMAGE, 0.8); 133 | } 134 | 135 | public boolean isLocked(BufferedImage image) { 136 | return ImageUtils.sameAs(image, M2_LOCKED_IMAGE, 0.8); 137 | } 138 | } 139 | 140 | }*/ 141 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/exception/AndroidDeviceException.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.exception; 2 | 3 | 4 | public class AndroidDeviceException extends NestedException { 5 | private static final long serialVersionUID = 5431510243540521938L; 6 | 7 | public AndroidDeviceException(String message) { 8 | super(message); 9 | } 10 | 11 | public AndroidDeviceException(Throwable t) { 12 | super(t); 13 | } 14 | 15 | public AndroidDeviceException(String message, Throwable t) { 16 | super(message, t); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/exception/DeviceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.exception; 2 | 3 | 4 | public class DeviceNotFoundException extends NestedException { 5 | 6 | /** 7 | * 8 | */ 9 | private static final long serialVersionUID = 5641289183499526194L; 10 | 11 | public DeviceNotFoundException(String msg) { 12 | super(msg); 13 | } 14 | 15 | public DeviceNotFoundException(String msg, Throwable cause) { 16 | super(msg, cause); 17 | } 18 | 19 | public DeviceNotFoundException(Throwable cause) { 20 | super(cause); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/exception/DeviceStoreException.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.exception; 2 | 3 | 4 | public class DeviceStoreException extends NestedException { 5 | private static final long serialVersionUID = 5431510243540521938L; 6 | 7 | public DeviceStoreException(String message) { 8 | super(message); 9 | } 10 | 11 | public DeviceStoreException(Throwable t) { 12 | super(t); 13 | } 14 | 15 | public DeviceStoreException(String message, Throwable t) { 16 | super(message, t); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/exception/DeviceUnlockException.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.exception; 2 | 3 | 4 | /** 5 | * 6 | * @author ltyao 7 | * 8 | */ 9 | public class DeviceUnlockException extends NestedException { 10 | 11 | public DeviceUnlockException(String msg) { 12 | super(msg); 13 | } 14 | 15 | /** 16 | * 17 | */ 18 | private static final long serialVersionUID = -5487201673781343155L; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/exception/NestedException.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.exception; 2 | 3 | /** 4 | * 5 | * @author ltyao 6 | * 7 | */ 8 | public class NestedException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = 5439915454935047936L; 11 | 12 | static { 13 | // Eagerly load the NestedExceptionUtils class to avoid classloader 14 | // deadlock 15 | // issues on OSGi when calling getMessage(). Reported by Don Brown; 16 | // SPR-5607. 17 | NestedExceptionUtils.class.getName(); 18 | } 19 | 20 | /** 21 | * Construct a {@code NestedRuntimeException} with the specified detail 22 | * message. 23 | * 24 | * @param msg 25 | * the detail message 26 | */ 27 | public NestedException(String msg) { 28 | super(msg); 29 | } 30 | 31 | /** 32 | * Construct a {@code NestedRuntimeException} with the specified detail 33 | * message and nested exception. 34 | * 35 | * @param msg 36 | * the detail message 37 | * @param cause 38 | * the nested exception 39 | */ 40 | public NestedException(String msg, Throwable cause) { 41 | super(msg, cause); 42 | } 43 | 44 | public NestedException(Throwable cause) { 45 | super(cause); 46 | } 47 | 48 | /** 49 | * Return the detail message, including the message from the nested 50 | * exception if there is one. 51 | */ 52 | @Override 53 | public String getMessage() { 54 | return NestedExceptionUtils 55 | .buildMessage(super.getMessage(), getCause()); 56 | } 57 | 58 | /** 59 | * Retrieve the innermost cause of this exception, if any. 60 | * 61 | * @return the innermost exception, or {@code null} if none 62 | * @since 2.0 63 | */ 64 | public Throwable getRootCause() { 65 | Throwable rootCause = null; 66 | Throwable cause = getCause(); 67 | while (cause != null && cause != rootCause) { 68 | rootCause = cause; 69 | cause = cause.getCause(); 70 | } 71 | return rootCause; 72 | } 73 | 74 | /** 75 | * Retrieve the most specific cause of this exception, that is, either the 76 | * innermost cause (root cause) or this exception itself. 77 | *

78 | * Differs from {@link #getRootCause()} in that it falls back to the present 79 | * exception if there is no root cause. 80 | * 81 | * @return the most specific cause (never {@code null}) 82 | * @since 2.0.3 83 | */ 84 | public Throwable getMostSpecificCause() { 85 | Throwable rootCause = getRootCause(); 86 | return (rootCause != null ? rootCause : this); 87 | } 88 | 89 | /** 90 | * Check whether this exception contains an exception of the given type: 91 | * either it is of the given class itself or it contains a nested cause of 92 | * the given type. 93 | * 94 | * @param exType 95 | * the exception type to look for 96 | * @return whether there is a nested exception of the specified type 97 | */ 98 | @SuppressWarnings("rawtypes") 99 | public boolean contains(Class exType) { 100 | if (exType == null) { 101 | return false; 102 | } 103 | if (exType.isInstance(this)) { 104 | return true; 105 | } 106 | Throwable cause = getCause(); 107 | if (cause == this) { 108 | return false; 109 | } 110 | if (cause instanceof NestedException) { 111 | return ((NestedException) cause).contains(exType); 112 | } else { 113 | while (cause != null) { 114 | if (exType.isInstance(cause)) { 115 | return true; 116 | } 117 | if (cause.getCause() == cause) { 118 | break; 119 | } 120 | cause = cause.getCause(); 121 | } 122 | return false; 123 | } 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/exception/NestedExceptionUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2008 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.cosysoft.device.exception; 18 | 19 | /** 20 | * Helper class for implementing exception classes which are capable of 21 | * holding nested exceptions. Necessary because we can't share a base 22 | * class among different exception types. 23 | * 24 | *

Mainly for use within the framework. 25 | * 26 | * @author Juergen Hoeller 27 | * @since 2.0 28 | */ 29 | public abstract class NestedExceptionUtils { 30 | 31 | /** 32 | * Build a message for the given base message and root cause. 33 | * @param message the base message 34 | * @param cause the root cause 35 | * @return the full exception message 36 | */ 37 | public static String buildMessage(String message, Throwable cause) { 38 | if (cause != null) { 39 | StringBuilder sb = new StringBuilder(); 40 | if (message != null) { 41 | sb.append(message).append("; "); 42 | } 43 | sb.append("nested exception is ").append(cause); 44 | return sb.toString(); 45 | } 46 | else { 47 | return message; 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/image/ImageUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.image; 2 | 3 | import com.android.ddmlib.RawImage; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import javax.imageio.ImageIO; 8 | import java.awt.*; 9 | import java.awt.image.*; 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.util.Hashtable; 14 | 15 | /** 16 | * @author ltyao 17 | */ 18 | public class ImageUtils { 19 | 20 | static final Logger logger = LoggerFactory.getLogger(ImageUtils.class); 21 | private static Hashtable EMPTY_HASH = new Hashtable<>(); 22 | private static int[] BAND_OFFSETS_32 = {0, 1, 2, 3}; 23 | private static int[] BAND_OFFSETS_16 = {0, 1}; 24 | 25 | public static BufferedImage convertImage(RawImage rawImage) { 26 | switch (rawImage.bpp) { 27 | case 16: 28 | return rawImage16toARGB(rawImage); 29 | case 32: 30 | return rawImage32toARGB(rawImage); 31 | } 32 | return null; 33 | } 34 | 35 | static int getMask(int length) { 36 | int res = 0; 37 | for (int i = 0; i < length; i++) { 38 | res = (res << 1) + 1; 39 | } 40 | return res; 41 | } 42 | 43 | private static BufferedImage rawImage32toARGB(RawImage rawImage) { 44 | DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, 45 | rawImage.size); 46 | 47 | PixelInterleavedSampleModel sampleModel = new PixelInterleavedSampleModel( 48 | 0, rawImage.width, rawImage.height, 4, rawImage.width * 4, 49 | BAND_OFFSETS_32); 50 | 51 | WritableRaster raster = Raster.createWritableRaster(sampleModel, 52 | dataBuffer, new Point(0, 0)); 53 | 54 | return new BufferedImage(new ThirtyTwoBitColorModel(rawImage), raster, 55 | false, EMPTY_HASH); 56 | } 57 | 58 | private static BufferedImage rawImage16toARGB(RawImage rawImage) { 59 | DataBufferByte dataBuffer = new DataBufferByte(rawImage.data, 60 | rawImage.size); 61 | 62 | PixelInterleavedSampleModel sampleModel = new PixelInterleavedSampleModel( 63 | 0, rawImage.width, rawImage.height, 2, rawImage.width * 2, 64 | BAND_OFFSETS_16); 65 | 66 | WritableRaster raster = Raster.createWritableRaster(sampleModel, 67 | dataBuffer, new Point(0, 0)); 68 | 69 | return new BufferedImage(new SixteenBitColorModel(rawImage), raster, 70 | false, EMPTY_HASH); 71 | } 72 | 73 | public static byte[] toByteArray(BufferedImage image) { 74 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 75 | try { 76 | ImageIO.write(image, "gif", out); 77 | return out.toByteArray(); 78 | } catch (IOException e) { 79 | throw new RuntimeException("", e); 80 | } 81 | } 82 | 83 | public static void writeToFile(BufferedImage image, String filePath) { 84 | 85 | try { 86 | ImageIO.write(image, "png", new File(filePath)); 87 | } catch (IOException e) { 88 | throw new RuntimeException(e); 89 | } 90 | } 91 | 92 | public static boolean sameAs(BufferedImage myImage, 93 | BufferedImage otherImage, double percent) { 94 | long start = System.currentTimeMillis(); 95 | 96 | int width = myImage.getWidth(); 97 | int height = myImage.getHeight(); 98 | 99 | int numDiffPixels = 0; 100 | for (int y = 0; y < height; y++) { 101 | for (int x = 0; x < width; x++) { 102 | if (myImage.getRGB(x, y) != otherImage.getRGB(x, y)) { 103 | numDiffPixels++; 104 | } 105 | } 106 | } 107 | double numberPixels = height * width; 108 | double rs = 1.0 - (int) ((numDiffPixels / numberPixels) * 100) / 100.0; 109 | 110 | long now = System.currentTimeMillis(); 111 | logger.debug("picture compare spend {} millseconds and result is", now 112 | - start, rs); 113 | return rs >= percent; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/image/SixteenBitColorModel.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.image; 2 | 3 | import java.awt.color.ColorSpace; 4 | import java.awt.image.ColorModel; 5 | import java.awt.image.Raster; 6 | 7 | import com.android.ddmlib.RawImage; 8 | 9 | class SixteenBitColorModel extends ColorModel { 10 | private static final int[] BITS = { 8, 8, 8, 8 }; 11 | 12 | public SixteenBitColorModel(RawImage rawImage) { 13 | super(32, BITS, ColorSpace.getInstance(1000), true, false, 3, 0); 14 | } 15 | 16 | public boolean isCompatibleRaster(Raster raster) { 17 | return true; 18 | } 19 | 20 | private int getPixel(Object inData) { 21 | byte[] data = (byte[]) inData; 22 | int value = data[0] & 0xFF; 23 | value |= data[1] << 8 & 0xFF00; 24 | 25 | return value; 26 | } 27 | 28 | public int getAlpha(Object inData) { 29 | return 255; 30 | } 31 | 32 | public int getBlue(Object inData) { 33 | int pixel = getPixel(inData); 34 | return (pixel >> 0 & 0x1F) << 3; 35 | } 36 | 37 | public int getGreen(Object inData) { 38 | int pixel = getPixel(inData); 39 | return (pixel >> 5 & 0x3F) << 2; 40 | } 41 | 42 | public int getRed(Object inData) { 43 | int pixel = getPixel(inData); 44 | return (pixel >> 11 & 0x1F) << 3; 45 | } 46 | 47 | public int getAlpha(int pixel) { 48 | throw new UnsupportedOperationException(); 49 | } 50 | 51 | public int getBlue(int pixel) { 52 | throw new UnsupportedOperationException(); 53 | } 54 | 55 | public int getGreen(int pixel) { 56 | throw new UnsupportedOperationException(); 57 | } 58 | 59 | public int getRed(int pixel) { 60 | throw new UnsupportedOperationException(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/image/ThirtyTwoBitColorModel.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.image; 2 | 3 | import java.awt.color.ColorSpace; 4 | import java.awt.image.ColorModel; 5 | import java.awt.image.Raster; 6 | 7 | import com.android.ddmlib.RawImage; 8 | 9 | class ThirtyTwoBitColorModel extends ColorModel { 10 | private static final int[] BITS = { 8, 8, 8, 8 }; 11 | private final int alphaLength; 12 | private final int alphaMask; 13 | private final int alphaOffset; 14 | private final int blueMask; 15 | private final int blueLength; 16 | private final int blueOffset; 17 | private final int greenMask; 18 | private final int greenLength; 19 | private final int greenOffset; 20 | private final int redMask; 21 | private final int redLength; 22 | private final int redOffset; 23 | 24 | public ThirtyTwoBitColorModel(RawImage rawImage) { 25 | super(32, BITS, ColorSpace.getInstance(1000), true, false, 3, 0); 26 | 27 | this.redOffset = rawImage.red_offset; 28 | this.redLength = rawImage.red_length; 29 | this.redMask = ImageUtils.getMask(this.redLength); 30 | this.greenOffset = rawImage.green_offset; 31 | this.greenLength = rawImage.green_length; 32 | this.greenMask = ImageUtils.getMask(this.greenLength); 33 | this.blueOffset = rawImage.blue_offset; 34 | this.blueLength = rawImage.blue_length; 35 | this.blueMask = ImageUtils.getMask(this.blueLength); 36 | this.alphaLength = rawImage.alpha_length; 37 | this.alphaOffset = rawImage.alpha_offset; 38 | this.alphaMask = ImageUtils.getMask(this.alphaLength); 39 | } 40 | 41 | public boolean isCompatibleRaster(Raster raster) { 42 | return true; 43 | } 44 | 45 | private int getPixel(Object inData) { 46 | byte[] data = (byte[]) inData; 47 | int value = data[0] & 0xFF; 48 | value |= (data[1] & 0xFF) << 8; 49 | value |= (data[2] & 0xFF) << 16; 50 | value |= (data[3] & 0xFF) << 24; 51 | 52 | return value; 53 | } 54 | 55 | public int getAlpha(Object inData) { 56 | int pixel = getPixel(inData); 57 | if (this.alphaLength == 0) { 58 | return 255; 59 | } 60 | return (pixel >>> this.alphaOffset & this.alphaMask) << 8 - this.alphaLength; 61 | } 62 | 63 | public int getBlue(Object inData) { 64 | int pixel = getPixel(inData); 65 | return (pixel >>> this.blueOffset & this.blueMask) << 8 - this.blueLength; 66 | } 67 | 68 | public int getGreen(Object inData) { 69 | int pixel = getPixel(inData); 70 | return (pixel >>> this.greenOffset & this.greenMask) << 8 - this.greenLength; 71 | } 72 | 73 | public int getRed(Object inData) { 74 | int pixel = getPixel(inData); 75 | return (pixel >>> this.redOffset & this.redMask) << 8 - this.redLength; 76 | } 77 | 78 | public int getAlpha(int pixel) { 79 | throw new UnsupportedOperationException(); 80 | } 81 | 82 | public int getBlue(int pixel) { 83 | throw new UnsupportedOperationException(); 84 | } 85 | 86 | public int getGreen(int pixel) { 87 | throw new UnsupportedOperationException(); 88 | } 89 | 90 | public int getRed(int pixel) { 91 | throw new UnsupportedOperationException(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/model/AppInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.model; 2 | 3 | public class AppInfo { 4 | 5 | private String packageURI; 6 | private String basePackage; 7 | private String mainActivity; 8 | private String version; 9 | 10 | public String getPackageURI() { 11 | return packageURI; 12 | } 13 | 14 | public void setPackageURI(String packageURI) { 15 | this.packageURI = packageURI; 16 | } 17 | 18 | public String getVersion() { 19 | return version; 20 | } 21 | 22 | public void setVersion(String version) { 23 | this.version = version; 24 | } 25 | 26 | public String getBasePackage() { 27 | return basePackage; 28 | } 29 | 30 | public void setBasePackage(String basePackage) { 31 | this.basePackage = basePackage; 32 | } 33 | 34 | public String getMainActivity() { 35 | return mainActivity; 36 | } 37 | 38 | public void setMainActivity(String mainActivity) { 39 | this.mainActivity = mainActivity; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/model/ClientDataInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.model; 2 | 3 | import com.android.ddmlib.Client; 4 | import com.android.ddmlib.ClientData; 5 | import java.util.Locale; 6 | 7 | /** 8 | * @author 兰天 9 | */ 10 | @Deprecated 11 | public class ClientDataInfo { 12 | 13 | private final transient ClientData clientData; 14 | private final transient Client client; 15 | 16 | private String name; 17 | private Integer pid; 18 | private Integer port; 19 | 20 | public ClientDataInfo(Client client) { 21 | this.clientData = client.getClientData(); 22 | this.client = client; 23 | this.name = name(); 24 | this.pid = clientData.getPid(); 25 | this.port = port(); 26 | } 27 | 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | public Integer getPid() { 33 | return pid; 34 | } 35 | 36 | public Integer getPort() { 37 | return port; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return "ClientDataInfo [name=" + name + ", pid=" + pid + ", port=" 43 | + port + "]"; 44 | } 45 | 46 | protected String name() { 47 | String name = clientData.getClientDescription(); 48 | if (name != null) { 49 | if ((clientData.isValidUserId()) && (clientData.getUserId() != 0)) { 50 | return String.format(Locale.CHINA, "%s (%d)", new Object[] { 51 | name, Integer.valueOf(clientData.getUserId())}); 52 | } 53 | return name; 54 | } 55 | return "?"; 56 | } 57 | 58 | protected Integer port() { 59 | int port = client.getDebuggerListenPort(); 60 | return port; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/model/DeviceInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.model; 2 | 3 | public class DeviceInfo { 4 | 5 | private String name; 6 | private String serial; 7 | private String osName; 8 | private String kernel; 9 | private String ram; 10 | private String rom; 11 | private String cpu; 12 | 13 | private Integer battery; 14 | private Integer density; 15 | 16 | 17 | public String getSerial() { 18 | return serial; 19 | } 20 | 21 | public void setSerial(String serial) { 22 | this.serial = serial; 23 | } 24 | 25 | public Integer getBattery() { 26 | return battery; 27 | } 28 | 29 | public void setBattery(Integer battery) { 30 | this.battery = battery; 31 | } 32 | 33 | public Integer getDensity() { 34 | return density; 35 | } 36 | 37 | public void setDensity(Integer density) { 38 | this.density = density; 39 | } 40 | 41 | public String getName() { 42 | return name; 43 | } 44 | 45 | public void setName(String name) { 46 | this.name = name; 47 | } 48 | 49 | public String getOsName() { 50 | return osName; 51 | } 52 | 53 | public void setOsName(String osName) { 54 | this.osName = osName; 55 | } 56 | 57 | public String getKernel() { 58 | return kernel; 59 | } 60 | 61 | public void setKernel(String kernel) { 62 | this.kernel = kernel; 63 | } 64 | 65 | public String getRam() { 66 | return ram; 67 | } 68 | 69 | public void setRam(String ram) { 70 | this.ram = ram; 71 | } 72 | 73 | public String getRom() { 74 | return rom; 75 | } 76 | 77 | public void setRom(String rom) { 78 | this.rom = rom; 79 | } 80 | 81 | public String getCpu() { 82 | return cpu; 83 | } 84 | 85 | public void setCpu(String cpu) { 86 | this.cpu = cpu; 87 | } 88 | 89 | @Override 90 | public boolean equals(Object o) { 91 | if (this == o) return true; 92 | if (o == null || getClass() != o.getClass()) return false; 93 | 94 | DeviceInfo that = (DeviceInfo) o; 95 | 96 | return serial.equals(that.serial); 97 | 98 | } 99 | 100 | @Override 101 | public int hashCode() { 102 | return serial.hashCode(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/shell/AndroidSdk.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.shell; 2 | 3 | import static com.github.cosysoft.device.shell.OS.platformExecutableSuffixBat; 4 | import static com.github.cosysoft.device.shell.OS.platformExecutableSuffixExe; 5 | 6 | import java.io.File; 7 | import java.io.FileFilter; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | 11 | public class AndroidSdk { 12 | 13 | public static final String ANDROID_FOLDER_PREFIX = "android-"; 14 | public static final String ANDROID_HOME = "ANDROID_HOME"; 15 | 16 | public static File adb() { 17 | 18 | return new File(platformToolsHome(), "adb" 19 | + platformExecutableSuffixExe()); 20 | } 21 | 22 | public static File aapt() throws AndroidSdkException { 23 | StringBuffer command = new StringBuffer(); 24 | command.append("aapt"); 25 | command.append(platformExecutableSuffixExe()); 26 | File platformToolsAapt = new File(platformToolsHome(), 27 | command.toString()); 28 | 29 | if (platformToolsAapt.isFile()) { 30 | return platformToolsAapt; 31 | } 32 | 33 | File buildToolsFolder = buildToolsHome(); 34 | 35 | return new File( 36 | findLatestAndroidPlatformFolder( 37 | buildToolsFolder, 38 | "Command 'aapt' was not found inside the Android SDK. Please update to the latest development tools and try again."), 39 | command.toString()); 40 | } 41 | 42 | public static File android() { 43 | StringBuffer command = new StringBuffer(); 44 | command.append(toolsHome()); 45 | 46 | return new File(toolsHome(), "android" + platformExecutableSuffixBat()); 47 | } 48 | 49 | public static File emulator() { 50 | return new File(toolsHome(), "emulator" + platformExecutableSuffixExe()); 51 | } 52 | 53 | private static File toolsHome() { 54 | StringBuffer command = new StringBuffer(); 55 | command.append(androidHome()); 56 | command.append(File.separator); 57 | command.append("tools"); 58 | command.append(File.separator); 59 | return new File(command.toString()); 60 | } 61 | 62 | private static File buildToolsHome() { 63 | StringBuffer command = new StringBuffer(); 64 | command.append(androidHome()); 65 | command.append(File.separator); 66 | command.append("build-tools"); 67 | command.append(File.separator); 68 | 69 | return new File(command.toString()); 70 | } 71 | 72 | private static File platformToolsHome() { 73 | StringBuffer command = new StringBuffer(); 74 | command.append(androidHome()); 75 | command.append(File.separator); 76 | command.append("platform-tools"); 77 | command.append(File.separator); 78 | return new File(command.toString()); 79 | } 80 | 81 | public static String androidHome() { 82 | String androidHome = System.getenv(ANDROID_HOME); 83 | 84 | if (androidHome == null) { 85 | throw new RuntimeException("Environment variable '" + ANDROID_HOME 86 | + "' was not found!"); 87 | } 88 | return androidHome; 89 | } 90 | 91 | /** 92 | * @return path to android.jar of latest android api. 93 | */ 94 | public static String androidJar() { 95 | String platformsRootFolder = androidHome() + File.separator 96 | + "platforms"; 97 | File platformsFolder = new File(platformsRootFolder); 98 | 99 | return new File(findLatestAndroidPlatformFolder(platformsFolder, 100 | "No installed Android APIs have been found."), "android.jar") 101 | .getAbsolutePath(); 102 | } 103 | 104 | protected static File findLatestAndroidPlatformFolder(File rootFolder, 105 | String errorMessage) { 106 | File[] androidApis = rootFolder.listFiles(new AndroidFileFilter()); 107 | if (androidApis == null || androidApis.length == 0) { 108 | throw new RuntimeException(errorMessage); 109 | } 110 | Arrays.sort(androidApis, Collections.reverseOrder()); 111 | return androidApis[0].getAbsoluteFile(); 112 | } 113 | 114 | public static class AndroidFileFilter implements FileFilter { 115 | 116 | @Override 117 | public boolean accept(File pathname) { 118 | String fileName = pathname.getName(); 119 | 120 | String regex = "\\d{2}\\.\\d{1}\\.\\d{1}"; 121 | if (fileName.matches(regex) 122 | || fileName.startsWith(ANDROID_FOLDER_PREFIX)) { 123 | return true; 124 | } 125 | return false; 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/shell/AndroidSdkException.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.shell; 2 | 3 | import com.github.cosysoft.device.exception.NestedException; 4 | 5 | public class AndroidSdkException extends NestedException { 6 | private static final long serialVersionUID = 5431510243540521938L; 7 | 8 | public AndroidSdkException(String message) { 9 | super(message); 10 | } 11 | 12 | public AndroidSdkException(Throwable t) { 13 | super(t); 14 | } 15 | 16 | public AndroidSdkException(String message, Throwable t) { 17 | super(message, t); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/shell/OS.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.shell; 2 | 3 | public class OS { 4 | public static boolean isWindows() { 5 | return System.getProperty("os.name").toLowerCase().indexOf("win") >= 0; 6 | } 7 | 8 | public static String platformExecutableSuffixExe() { 9 | return isWindows() ? ".exe" : ""; 10 | } 11 | 12 | public static String platformExecutableSuffixBat() { 13 | return isWindows() ? ".bat" : ""; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/shell/ShellCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.shell; 2 | 3 | import java.util.Map; 4 | 5 | import org.apache.commons.exec.CommandLine; 6 | import org.apache.commons.exec.DefaultExecuteResultHandler; 7 | import org.apache.commons.exec.DefaultExecutor; 8 | import org.apache.commons.exec.ExecuteResultHandler; 9 | import org.apache.commons.exec.ExecuteWatchdog; 10 | import org.apache.commons.exec.LogOutputStream; 11 | import org.apache.commons.exec.PumpStreamHandler; 12 | import org.apache.commons.exec.environment.EnvironmentUtils; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | 17 | public class ShellCommand { 18 | 19 | private static final Logger log = LoggerFactory.getLogger(ShellCommand.class 20 | .getName()); 21 | 22 | public static String exec(CommandLine commandLine) 23 | throws ShellCommandException { 24 | return exec(commandLine, 20000); 25 | } 26 | 27 | public static String exec(CommandLine commandline, long timeoutInMillies) 28 | throws ShellCommandException { 29 | log.debug("executing command: " + commandline); 30 | PritingLogOutputStream outputStream = new PritingLogOutputStream(); 31 | DefaultExecutor exec = new DefaultExecutor(); 32 | exec.setWatchdog(new ExecuteWatchdog(timeoutInMillies)); 33 | PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream); 34 | exec.setStreamHandler(streamHandler); 35 | try { 36 | exec.execute(commandline); 37 | } catch (Exception e) { 38 | throw new ShellCommandException( 39 | "An error occured while executing shell command: " 40 | + commandline, new ShellCommandException( 41 | outputStream.getOutput())); 42 | } 43 | return (outputStream.getOutput()); 44 | } 45 | 46 | public static void execAsync(CommandLine commandline) 47 | throws ShellCommandException { 48 | execAsync(null, commandline); 49 | } 50 | 51 | @SuppressWarnings("rawtypes") 52 | public static void execAsync(String display, CommandLine commandline) 53 | throws ShellCommandException { 54 | log.debug("executing async command: " + commandline); 55 | DefaultExecutor exec = new DefaultExecutor(); 56 | 57 | ExecuteResultHandler handler = new DefaultExecuteResultHandler(); 58 | PumpStreamHandler streamHandler = new PumpStreamHandler( 59 | new PritingLogOutputStream()); 60 | exec.setStreamHandler(streamHandler); 61 | try { 62 | if (display == null || display.isEmpty()) { 63 | exec.execute(commandline, handler); 64 | } else { 65 | Map env = EnvironmentUtils.getProcEnvironment(); 66 | EnvironmentUtils.addVariableToEnvironment(env, "DISPLAY=:" 67 | + display); 68 | 69 | exec.execute(commandline, env, handler); 70 | } 71 | } catch (Exception e) { 72 | throw new ShellCommandException( 73 | "An error occured while executing shell command: " 74 | + commandline, e); 75 | } 76 | } 77 | 78 | private static class PritingLogOutputStream extends LogOutputStream { 79 | 80 | private StringBuilder output = new StringBuilder(); 81 | 82 | @Override 83 | protected void processLine(String line, int level) { 84 | log.debug("OUTPUT FROM PROCESS: " + line); 85 | 86 | output.append(line).append("\n"); 87 | } 88 | 89 | public String getOutput() { 90 | return output.toString(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /device-api/src/main/java/com/github/cosysoft/device/shell/ShellCommandException.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.shell; 2 | 3 | import com.github.cosysoft.device.exception.NestedException; 4 | 5 | public class ShellCommandException extends NestedException { 6 | 7 | private static final long serialVersionUID = 268831360479853360L; 8 | 9 | public ShellCommandException(String message) { 10 | super(message); 11 | } 12 | 13 | public ShellCommandException(Throwable t) { 14 | super(t); 15 | } 16 | 17 | public ShellCommandException(String message, Throwable t) { 18 | super(message, t); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /device-api/src/main/resources/com/github/cosysoft/device/android/impl/automator.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosysoft/device/fffe79fdde5f1a07fe401f42206cef9098d0f661/device-api/src/main/resources/com/github/cosysoft/device/android/impl/automator.jar -------------------------------------------------------------------------------- /device-api/src/main/resources/com/github/cosysoft/device/android/impl/handlePopBox.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosysoft/device/fffe79fdde5f1a07fe401f42206cef9098d0f661/device-api/src/main/resources/com/github/cosysoft/device/android/impl/handlePopBox.jar -------------------------------------------------------------------------------- /device-api/src/main/resources/com/github/cosysoft/device/android/unlock_apk-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosysoft/device/fffe79fdde5f1a07fe401f42206cef9098d0f661/device-api/src/main/resources/com/github/cosysoft/device/android/unlock_apk-debug.apk -------------------------------------------------------------------------------- /device-api/src/main/resources/com/github/cosysoft/device/android/xiaomi/CapMI_1.0.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosysoft/device/fffe79fdde5f1a07fe401f42206cef9098d0f661/device-api/src/main/resources/com/github/cosysoft/device/android/xiaomi/CapMI_1.0.apk -------------------------------------------------------------------------------- /device-api/src/test/java/com/github/cosysoft/device/android/test/AndroidAppTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.test; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.io.File; 6 | 7 | import com.github.cosysoft.device.android.AndroidApp; 8 | import com.github.cosysoft.device.android.impl.DefaultAndroidApp; 9 | import org.junit.Test; 10 | 11 | public class AndroidAppTest { 12 | 13 | @Test 14 | public void testExtract() { 15 | AndroidApp app = new DefaultAndroidApp(new File("d:\\aut\\qua.apk")); 16 | 17 | assertEquals("com.taobao.trip", app.getBasePackage()); 18 | 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /device-api/src/test/java/com/github/cosysoft/device/android/test/AndroidDeviceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.test; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import java.util.TreeSet; 6 | 7 | import com.github.cosysoft.device.DeviceStore; 8 | import com.github.cosysoft.device.android.AndroidDevice; 9 | import com.github.cosysoft.device.android.impl.AndroidDeviceStore; 10 | import org.junit.AfterClass; 11 | import org.junit.BeforeClass; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | /** 16 | * super test for device 17 | * 18 | * @author ltyao 19 | * 20 | */ 21 | public class AndroidDeviceTest { 22 | 23 | protected static final Logger logger = LoggerFactory 24 | .getLogger(AndroidDeviceTest.class); 25 | protected static DeviceStore deviceStore; 26 | 27 | @BeforeClass 28 | public static void setUp() { 29 | deviceStore = AndroidDeviceStore.getInstance(); 30 | assertTrue("devices size must > 0", deviceStore.getDevices().size() > 0); 31 | } 32 | 33 | protected AndroidDevice pollFirst() { 34 | return deviceStore.getDevices().pollFirst(); 35 | } 36 | 37 | protected TreeSet getDevices() { 38 | return deviceStore.getDevices(); 39 | } 40 | 41 | @AfterClass 42 | public static void tearDown() { 43 | deviceStore.shutdownForcely(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /device-api/src/test/java/com/github/cosysoft/device/android/test/DeviceClientTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.test; 2 | 3 | import org.junit.Test; 4 | 5 | import com.android.ddmlib.Client; 6 | import com.android.ddmlib.ClientData; 7 | 8 | public class DeviceClientTest extends AndroidDeviceTest { 9 | 10 | @Test 11 | public void testClients() { 12 | Client[] clients = pollFirst().getDevice().getClients(); 13 | for (int i = 0; i < clients.length; i++) { 14 | ClientData clientData = clients[i].getClientData(); 15 | System.out.println(clientData.getClientDescription()); 16 | } 17 | } 18 | 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /device-api/src/test/java/com/github/cosysoft/device/android/test/DeviceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.test; 2 | 3 | import java.io.File; 4 | 5 | import com.github.cosysoft.device.android.AndroidApp; 6 | import com.github.cosysoft.device.android.AndroidDevice; 7 | import com.github.cosysoft.device.android.impl.DefaultAndroidApp; 8 | import org.junit.Test; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | /** 13 | * 14 | * @author ltyao 15 | * 16 | */ 17 | public class DeviceTest extends AndroidDeviceTest { 18 | 19 | static final Logger logger = LoggerFactory.getLogger(DeviceTest.class); 20 | 21 | @Test 22 | public void testGetProperties() { 23 | for (AndroidDevice device : getDevices()) { 24 | device.getDeviceInfo(); 25 | } 26 | } 27 | 28 | @Test 29 | public void testGetInfo() { 30 | for (AndroidDevice device : getDevices()) { 31 | logger.debug(device.getName().toUpperCase()); 32 | } 33 | } 34 | 35 | @Test 36 | public void testUnlock() { 37 | for (AndroidDevice device : getDevices()) { 38 | device.unlock(); 39 | } 40 | } 41 | 42 | @Test 43 | public void testInstallAndKill() throws InterruptedException { 44 | for (AndroidDevice device : getDevices()) { 45 | File xiaomi = new File("d:\\apkpack\\Ctrip_V5.10_SIT7.2_TEST.apk"); 46 | AndroidApp app = new DefaultAndroidApp(xiaomi); 47 | device.unlock(); 48 | device.uninstall(app); 49 | device.install(app); 50 | device.start(app); 51 | Thread.sleep(3000); 52 | device.kill(app); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /device-api/src/test/java/com/github/cosysoft/device/android/test/LogcatTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.test; 2 | 3 | import java.util.List; 4 | 5 | import com.github.cosysoft.device.android.AndroidDevice; 6 | import org.junit.Test; 7 | 8 | import com.android.ddmlib.Log.LogLevel; 9 | import com.android.ddmlib.logcat.LogCatFilter; 10 | import com.android.ddmlib.logcat.LogCatListener; 11 | import com.android.ddmlib.logcat.LogCatMessage; 12 | 13 | public class LogcatTest extends AndroidDeviceTest { 14 | 15 | @Test 16 | public void testLog() throws InterruptedException { 17 | AndroidDevice device = pollFirst(); 18 | 19 | final LogCatFilter filter = new LogCatFilter("", "", "com.android", "", 20 | "", LogLevel.WARN); 21 | final LogCatListener lcl = new LogCatListener() { 22 | @Override 23 | public void log(List msgList) { 24 | for (LogCatMessage msg : msgList) { 25 | if (filter.matches(msg)) { 26 | System.out.println(msg); 27 | } 28 | } 29 | } 30 | }; 31 | 32 | device.addLogCatListener(lcl); 33 | 34 | Thread.sleep(50000000); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /device-api/src/test/java/com/github/cosysoft/device/android/test/PerformanceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.test; 2 | 3 | import com.android.ddmlib.Client; 4 | import com.android.ddmlib.ClientData; 5 | import com.android.ddmlib.ThreadInfo; 6 | import com.github.cosysoft.device.android.AndroidDevice; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | public class PerformanceTest extends AndroidDeviceTest { 11 | 12 | private AndroidDevice device; 13 | 14 | @Before 15 | public void takeOne() throws InterruptedException { 16 | device = getDevices().pollFirst(); 17 | Thread.sleep(2000); //just wait 2 second at first time to collect client info 18 | } 19 | 20 | @Test 21 | public void testListClients() { 22 | 23 | Client[] clients = device.getAllClient(); 24 | for (Client client : clients) { 25 | ClientData clientData = client.getClientData(); 26 | System.out.println(clientData.getClientDescription() + " " + clientData.getPid()); 27 | } 28 | } 29 | 30 | @Test 31 | public void testListTheads() { 32 | 33 | Client runningApp = device.getClientByAppName("com.android.calendar"); 34 | 35 | ThreadInfo[] threads = runningApp.getClientData().getThreads(); 36 | 37 | for (int i = 0; i < threads.length; i++) { 38 | System.out.println(threads[i].getThreadName() 39 | + " at " 40 | + threads[i].getStatus()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /device-api/src/test/java/com/github/cosysoft/device/android/test/Readme.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.test; 2 | 3 | import com.android.ddmlib.Log.LogLevel; 4 | import com.android.ddmlib.logcat.LogCatFilter; 5 | import com.android.ddmlib.logcat.LogCatListener; 6 | import com.android.ddmlib.logcat.LogCatMessage; 7 | import com.github.cosysoft.device.android.AndroidApp; 8 | import com.github.cosysoft.device.android.AndroidDevice; 9 | import com.github.cosysoft.device.android.impl.AndroidDeviceStore; 10 | import com.github.cosysoft.device.android.impl.DefaultAndroidApp; 11 | import com.github.cosysoft.device.image.ImageUtils; 12 | import java.awt.image.BufferedImage; 13 | import java.io.File; 14 | import java.util.List; 15 | import java.util.TreeSet; 16 | import org.junit.Test; 17 | import org.slf4j.profiler.Profiler; 18 | 19 | public class Readme extends AndroidDeviceTest { 20 | 21 | @Test 22 | public void takeDevices() { 23 | TreeSet devices = AndroidDeviceStore.getInstance() 24 | .getDevices(); 25 | 26 | for (AndroidDevice d : devices) { 27 | System.out.println(d.getSerialNumber()); 28 | } 29 | AndroidDevice device = devices.pollFirst(); 30 | System.out.println(device.getName()); 31 | } 32 | 33 | @Test 34 | public void takeScreenshot() { 35 | 36 | AndroidDevice device = getDevices().pollFirst(); 37 | 38 | Profiler profiler = new Profiler("screen"); 39 | profiler.start("start"); 40 | BufferedImage image = device.takeScreenshot(); 41 | profiler.stop(); 42 | profiler.print(); 43 | String imagePath = new File(System.getProperty("java.io.tmpdir"), 44 | "screenshot.png").getAbsolutePath(); 45 | ImageUtils.writeToFile(image, imagePath); 46 | logger.debug("image saved to path {}", imagePath); 47 | } 48 | 49 | @Test 50 | public void installApp() { 51 | AndroidDevice device = getDevices().pollFirst(); 52 | 53 | AndroidApp app = new DefaultAndroidApp(new File( 54 | "d:\\uat\\com.android.chrome.apk")); 55 | device.install(app); 56 | if (device.isInstalled(app)) { 57 | device.uninstall(app); 58 | } 59 | } 60 | 61 | @Test 62 | public void testLogcat() throws InterruptedException { 63 | AndroidDevice device = getDevices().pollFirst(); 64 | 65 | final LogCatFilter filter = new LogCatFilter("", "", "com.android", "", 66 | "", LogLevel.WARN); 67 | final LogCatListener lcl = new LogCatListener() { 68 | @Override 69 | public void log(List msgList) { 70 | for (LogCatMessage msg : msgList) { 71 | if (filter.matches(msg)) { 72 | System.out.println(msg); 73 | } 74 | } 75 | } 76 | }; 77 | 78 | device.addLogCatListener(lcl); 79 | 80 | Thread.sleep(60000); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /device-api/src/test/java/com/github/cosysoft/device/android/test/XiaomiInstallerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.android.test; 2 | 3 | import java.io.File; 4 | 5 | import com.github.cosysoft.device.android.AndroidApp; 6 | import com.github.cosysoft.device.android.AndroidDevice; 7 | import com.github.cosysoft.device.android.impl.DefaultAndroidApp; 8 | import com.github.cosysoft.device.android.xiaomi.MIDeviceUtility; 9 | import org.junit.Test; 10 | 11 | /** 12 | * 13 | * @author ltyao 14 | * 15 | */ 16 | public class XiaomiInstallerTest extends AndroidDeviceTest { 17 | 18 | @Test 19 | public void testXiaomiInstall() { 20 | 21 | File ctrip = new File("D:\\apkpack\\UCBrowser_V9.8.1.447.apk"); 22 | AndroidApp app = new DefaultAndroidApp(ctrip); 23 | 24 | logger.info(app.getBasePackage()); 25 | logger.info(app.getMainActivity()); 26 | String activity = app.getMainActivity().replace(app.getBasePackage(), 27 | ""); 28 | 29 | logger.info(activity); 30 | for (AndroidDevice device : getDevices()) { 31 | MIDeviceUtility.testInstall(device); 32 | MIDeviceUtility.install(device, app); 33 | // device.install(app); 34 | } 35 | } 36 | 37 | @Test 38 | public void testImageSub() { 39 | for (AndroidDevice device : getDevices()) { 40 | logger.info(device.currentActivity()); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /device-keeper/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /device-keeper/app/app.css: -------------------------------------------------------------------------------- 1 | .hide { display: none !important; } 2 | 3 | body { 4 | overflow: hidden; 5 | max-width: 100%; 6 | max-height: 100%; 7 | } 8 | table { 9 | margin-bottom: 20px; 10 | max-width: 100%; 11 | width: 100%; 12 | border-spacing: 0; 13 | border-collapse: collapse; 14 | background-color: transparent; 15 | border-radius: 2px; 16 | overflow: hidden; 17 | } 18 | 19 | /*************** 20 | * TYPE DEFAULTS 21 | ***************/ 22 | a { 23 | color: #3f51b5; 24 | text-decoration: none; 25 | } 26 | a:hover, a:focus { 27 | text-decoration: underline; 28 | } 29 | h1, h2, h3, h4, h5, h6 { 30 | margin-bottom: 1rem; 31 | margin-top: 1rem; 32 | } 33 | h1 { 34 | font-size: 3.400rem; 35 | font-weight: 400; 36 | line-height: 4rem; 37 | } 38 | h2 { 39 | font-size: 2.400rem; 40 | font-weight: 400; 41 | line-height: 3.2rem; 42 | } 43 | h3 { 44 | font-size: 2.000rem; 45 | font-weight: 500; 46 | letter-spacing: 0.005em; 47 | } 48 | h4 { 49 | font-size: 1.600rem; 50 | font-weight: 400; 51 | letter-spacing: 0.010em; 52 | line-height: 2.4rem; 53 | } 54 | p { 55 | font-size: 1.6rem; 56 | font-weight: 400; 57 | letter-spacing: 0.010em; 58 | line-height: 2.2rem; 59 | margin: 1.6rem 0; 60 | } 61 | strong { 62 | font-weight: 500; 63 | } 64 | td, th { 65 | padding: 12px 8px; 66 | text-align: left; 67 | } 68 | td { 69 | vertical-align: top; 70 | } 71 | td.description *:first-child { 72 | margin-top: 0; 73 | } 74 | td.description *:last-child { 75 | margin-bottom: 0; 76 | } 77 | tr:nth-child(even) td { 78 | background-color: #f5f5f5; 79 | } 80 | th { 81 | border-bottom: 1px solid #ccc; 82 | background-color: #f5f5f5; 83 | } 84 | blockquote { 85 | border-left: 3px solid rgba(0, 0, 0, 0.12); 86 | font-style: italic; 87 | margin-left: 0; 88 | padding-left: 16px; 89 | } 90 | ul { 91 | margin: 0; 92 | padding: 0; 93 | } 94 | ul li { 95 | margin-left: 16px; 96 | padding: 0; 97 | margin-top: 3px; 98 | list-style-position: inside; 99 | } 100 | ul li:first-child { 101 | margin-top: 0; 102 | } 103 | /************ 104 | * UTILS 105 | ************/ 106 | ul.skip-links li { 107 | list-style: none; 108 | margin: 0; 109 | padding: 0; 110 | } 111 | ul.skip-links li a { 112 | background-color: #fff; 113 | display: block; 114 | margin: 0.5em 0 0.5em 0.5em; 115 | opacity: 0; 116 | left: 0; 117 | position: absolute; 118 | text-decoration: none; 119 | top: 0; 120 | width: 92%; 121 | -webkit-transition: opacity 0.15s linear; 122 | -moz-transition: opacity 0.15s linear; 123 | transition: opacity 0.15s linear; 124 | } 125 | ul.skip-links li a:focus { 126 | background-color: #fff !important; 127 | opacity: 1; 128 | z-index: 2; 129 | } 130 | 131 | /** 132 | **/ 133 | .menuBtn { 134 | background-color: transparent; 135 | border: none; 136 | height: 38px; 137 | margin: 16px 0 0 16px; 138 | width: 36px; 139 | } 140 | md-toolbar h1 { 141 | font-weight: normal; 142 | } 143 | md-list .md-button { 144 | color: inherit; 145 | font-weight: 500; 146 | text-align: left; 147 | width: 100%; 148 | } 149 | 150 | /* Using Data-URI converted from svg until becomes available 151 | https://github.com/google/material-design-icons 152 | */ 153 | .menuBtn { 154 | background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4IiB2aWV3Qm94PSIwIDAgMjQgMjQiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI0IDI0IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGcgaWQ9IkhlYWRlciI+CiAgICA8Zz4KICAgICAgICA8cmVjdCB4PSItNjE4IiB5PSItMjIzMiIgZmlsbD0ibm9uZSIgd2lkdGg9IjE0MDAiIGhlaWdodD0iMzYwMCIvPgogICAgPC9nPgo8L2c+CjxnIGlkPSJMYWJlbCI+CjwvZz4KPGcgaWQ9Ikljb24iPgogICAgPGc+CiAgICAgICAgPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+CiAgICAgICAgPHBhdGggZD0iTTMsMThoMTh2LTJIM1YxOHogTTMsMTNoMTh2LTJIM1YxM3ogTTMsNnYyaDE4VjZIM3oiIHN0eWxlPSJmaWxsOiNmM2YzZjM7Ii8+CiAgICA8L2c+CjwvZz4KPGcgaWQ9IkdyaWQiIGRpc3BsYXk9Im5vbmUiPgogICAgPGcgZGlzcGxheT0iaW5saW5lIj4KICAgIDwvZz4KPC9nPgo8L3N2Zz4=) no-repeat center center; 155 | } 156 | 157 | 158 | .docs-menu, 159 | .docs-menu ul { 160 | list-style: none; 161 | padding: 0; 162 | } 163 | .docs-menu li { 164 | margin: 0; 165 | } 166 | .docs-menu > li { 167 | border-top: 1px solid rgba(0, 0, 0, 0.12); 168 | } 169 | .docs-menu .md-button { 170 | border-radius: 0; 171 | color: inherit; 172 | cursor: pointer; 173 | display: block; 174 | line-height: 40px; 175 | margin: 0; 176 | max-height: 40px; 177 | overflow: hidden; 178 | padding: 0px 16px; 179 | text-align: left; 180 | text-decoration: none; 181 | white-space: normal; 182 | width: 100%; 183 | } 184 | .docs-menu button.md-button::-moz-focus-inner { 185 | padding: 0; 186 | } 187 | .docs-menu .md-button.active { 188 | color: #03a9f4; 189 | } 190 | .menu-heading { 191 | display: block; 192 | line-height: 40px; 193 | margin: 0; 194 | padding: 0px 16px; 195 | text-align: left; 196 | width: 100%; 197 | } 198 | .docs-menu li.parentActive, 199 | .docs-menu li.parentActive .menu-toggle-list { 200 | background-color: #f6f6f6; 201 | } 202 | .menu-toggle-list { 203 | background: #fff; 204 | max-height: 1300px; 205 | overflow: hidden; 206 | position: relative; 207 | z-index: 1; 208 | -webkit-transition: 0.75s cubic-bezier(0.35, 0, 0.25, 1); 209 | -webkit-transition-property: max-height; 210 | -moz-transition: 0.75s cubic-bezier(0.35, 0, 0.25, 1); 211 | -moz-transition-property: max-height; 212 | transition: 0.75s cubic-bezier(0.35, 0, 0.25, 1); 213 | transition-property: max-height; 214 | } 215 | .menu-toggle-list.ng-hide { 216 | max-height: 0; 217 | } 218 | .docs-menu .menu-toggle-list a.md-button { 219 | display: block; 220 | font-weight: 400; 221 | padding: 0 16px 0 32px; 222 | text-transform: none; 223 | } 224 | .md-button-toggle .md-toggle-icon { 225 | display: block; 226 | margin-left: auto; 227 | speak: none; 228 | vertical-align: middle; 229 | transform: rotate(180deg); 230 | -webkit-transform: rotate(180deg); 231 | transition: transform 0.3s ease-in-out; 232 | -webkit-transition: -webkit-transform 0.3s ease-in-out; 233 | } 234 | .md-button-toggle .md-toggle-icon.toggled { 235 | transform: rotate(0deg); 236 | -webkit-transform: rotate(0deg); 237 | } 238 | /* End Docs Menu */ 239 | 240 | .docs-logo:focus { 241 | outline: none; 242 | } 243 | .docs-logo md-icon { 244 | height: 40px; 245 | margin: 0; 246 | width: 40px; 247 | } 248 | .docs-logotype { 249 | line-height: 40px; 250 | text-indent: 15px; 251 | } 252 | .docs-menu-icon { 253 | background: none; 254 | border: none; 255 | margin-right: 16px; 256 | padding: 0; 257 | } 258 | .docs-menu-separator-icon { 259 | margin: 0; 260 | padding: 0 10px; 261 | } 262 | .docs-menu-separator-icon img { 263 | height: 16px; 264 | } 265 | -------------------------------------------------------------------------------- /device-keeper/app/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 兰天 on 2015/3/21. 3 | */ 4 | var app = angular.module('KeeperApp', ['ngMaterial', 'ngRoute', 'deviceControllers']); 5 | 6 | app.config(['$routeProvider', 7 | function($routeProvider) { 8 | $routeProvider. 9 | when('/device', { 10 | templateUrl: 'device/device-list.html', 11 | controller: 'DeviceCtrl' 12 | }). 13 | when('/node', { 14 | templateUrl: 'device/node-list.html' 15 | }). 16 | otherwise({ 17 | redirectTo: 'device' 18 | }); 19 | } 20 | ]); 21 | 22 | app.controller('AppCtrl', ['$scope', '$mdSidenav', function($scope, $mdSidenav) { 23 | $scope.toggleSidenav = function(menuId) { 24 | $mdSidenav(menuId).toggle(); 25 | }; 26 | 27 | $scope.menus = [{ 28 | name: 'Device List', 29 | hash: '/device' 30 | }, { 31 | name: 'Node List', 32 | hash: '/node' 33 | }]; 34 | 35 | 36 | $scope.isSectionSelected = function(section) { 37 | var selected = false; 38 | var openedSection = $scope.menus.openedSection; 39 | if (openedSection === section) { 40 | selected = true; 41 | } 42 | return selected; 43 | } 44 | }]); 45 | -------------------------------------------------------------------------------- /device-keeper/app/controller.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 兰天 on 2015/3/31. 3 | */ 4 | var deviceControllers = angular.module('deviceControllers', []); 5 | 6 | deviceControllers.controller('DeviceCtrl', ['$scope', '$http', 7 | function($scope, $http) { 8 | $http.get('hub/device').success(function(data) { 9 | $scope.devices = data; 10 | }); 11 | } 12 | ]); 13 | -------------------------------------------------------------------------------- /device-keeper/app/device/device-list.html: -------------------------------------------------------------------------------- 1 | 4 | 6 |

{{device.name}}

7 | {{device.did}} 8 | 9 | -------------------------------------------------------------------------------- /device-keeper/app/device/node-list.html: -------------------------------------------------------------------------------- 1 | Node list -------------------------------------------------------------------------------- /device-keeper/app/i2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 26 | 27 | 28 | 29 | 31 | 32 | 35 |

Android Device Keeper

36 |
37 | 38 | 39 | 44 | 45 |
46 | 47 | 48 |
49 | 50 | 51 | 52 |
53 | 54 | 58 | 60 | 61 |

#1: (3r x 2c)

62 |
63 |
64 | 65 | 66 |

#2: (1r x 1c)

67 |
68 |
69 | 70 | 71 |

#3: (1r x 1c)

72 |
73 |
74 | 76 | 77 |

#4: (2r x 1c)

78 |
79 |
80 | 82 | 83 |

#5: (2r x 2c)

84 |
85 |
86 | 88 | 89 |

#6: (2r x 1c)

90 |
91 |
93 | 94 |

#6: (2r x 1c)

95 |
96 |
98 | 99 |

#6: (2r x 1c)

100 |
101 |
103 | 104 |

#6: (2r x 1c)

105 |
106 |
108 | 109 |

#6: (2r x 1c)

110 |
111 |
113 | 114 |

#6: (2r x 1c)

115 |
116 |
118 | 119 |

#6: (2r x 1c)

120 |
121 |
123 | 124 |

#6: (2r x 1c)

125 |
126 |
127 |
128 |
129 |
130 |
131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /device-keeper/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 18 |

Android Device Keeper

19 |
20 | 21 | 22 | 27 | 28 |
29 | 30 | 31 |
32 | 33 | 34 | 35 |
36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /device-keeper/app/mock/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cosysoft/device/fffe79fdde5f1a07fe401f42206cef9098d0f661/device-keeper/app/mock/avatar.png -------------------------------------------------------------------------------- /device-keeper/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "device-keeper", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/cosysoft/device", 5 | "authors": [ 6 | "Bluesky Yao " 7 | ], 8 | "license": "Apache", 9 | "private": true, 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "angular-route": "~1.4.8", 19 | "angular-material": "~1.0.0", 20 | "underscore": "^1.8.2" 21 | }, 22 | "resolutions": { 23 | "angular": "1.4.8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /device-keeper/keeper/device.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 兰天 on 2015/3/30. 3 | */ 4 | var _ = require('underscore'); 5 | 6 | var Device = function() { 7 | this.devices = [{ 8 | name: "xiaomi-2013022-ZDPB8PVCO7QGYDQG", 9 | udid: "ZDPB8PVCO7QGYDQG", 10 | avatarUri: "mock/avatar.png", 11 | osName: "ANDROID17(4.2.2)", 12 | nodeUri: 'http://172.0.0.1:8080/', 13 | nodeName: 'Windows8' 14 | }, { 15 | name: "xiaomi-2013022-ZDPB8PVCO7QGYDQG", 16 | udid: "ZDPB8PVCO7QGYDQG", 17 | avatarUri: "mock/avatar.png", 18 | osName: "ANDROID17(4.2.2)", 19 | nodeUri: 'http://172.0.0.1:8080/', 20 | nodeName: 'Windows8' 21 | }, { 22 | name: "xiaomi-2013022-ZDPB8PVCO7QGYDQG", 23 | udid: "ZDPB8PVCO7QGYDQG", 24 | avatarUri: "mock/avatar.png", 25 | osName: "ANDROID17(4.2.2)", 26 | nodeUri: 'http://172.0.0.1:8080/', 27 | nodeName: 'Windows8' 28 | }, { 29 | name: "xiaomi-2013022-ZDPB8PVCO7QGYDQG", 30 | udid: "ZDPB8PVCO7QGYDQG", 31 | avatarUri: "mock/avatar.png", 32 | osName: "ANDROID17(4.2.2)", 33 | nodeUri: 'http://172.0.0.1:8080/', 34 | nodeName: 'Windows8' 35 | }, { 36 | name: "xiaomi-2013022-ZDPB8PVCO7QGYDQG", 37 | udid: "ZDPB8PVCO7QGYDQG", 38 | avatarUri: "mock/avatar.png", 39 | osName: "ANDROID17(4.2.2)", 40 | nodeUri: 'http://172.0.0.1:8080/', 41 | nodeName: 'Windows8' 42 | }, { 43 | name: "xiaomi-2013022-ZDPB8PVCO7QGYDQG", 44 | udid: "ZDPB8PVCO7QGYDQG", 45 | avatarUri: "mock/avatar.png", 46 | osName: "ANDROID17(4.2.2)", 47 | nodeUri: 'http://172.0.0.1:8080/', 48 | nodeName: 'Windows8' 49 | }]; 50 | }; 51 | 52 | Device.prototype.add = function(node) { 53 | var od = _.find(this.devices, function(d) { 54 | return d.udid === node.udid; 55 | }); 56 | if (od) { 57 | _.extend(od, node); 58 | } else { 59 | this.devices.push(node); 60 | } 61 | }; 62 | 63 | Device.prototype.toArray = function() { 64 | return this.devices; 65 | }; 66 | 67 | module.exports = Device; 68 | -------------------------------------------------------------------------------- /device-keeper/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by 兰天 on 2015/3/30. 3 | */ 4 | var express = require('express'); 5 | var Device = require('./keeper/device'); 6 | 7 | var app = express(); 8 | var device = new Device(); 9 | 10 | 11 | app.use(express.static('app')); 12 | 13 | app.get('/api', function (req, res) { 14 | res.send('Hello World!') 15 | }) 16 | app.get('/hub/device', function (req, res) { 17 | res.json(device.toArray()); 18 | }); 19 | 20 | var server = app.listen(3000, function () { 21 | 22 | var host = server.address().address 23 | var port = server.address().port 24 | 25 | console.log('Example app listening at http://%s:%s', host, port) 26 | 27 | console.log('http://localhost:3000/static/'); 28 | }); -------------------------------------------------------------------------------- /device-keeper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keeper", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node main.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "express": "^4.12.3", 14 | "underscore": "^1.8.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/Application.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.ApplicationContext; 8 | import org.springframework.context.annotation.ComponentScan; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | /** 14 | * Created by 兰天 on 2015/4/5. 15 | */ 16 | @SpringBootApplication 17 | @RestController 18 | @ComponentScan(basePackages = "com.github.cosysoft.device.node") 19 | @EnableScheduling 20 | @RequestMapping("/api/version") 21 | public class Application { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(Application.class); 24 | 25 | public static void main(String[] args) throws Exception { 26 | ApplicationContext applicationContext = SpringApplication.run(Application.class, args); 27 | } 28 | 29 | @RequestMapping 30 | public String version() { 31 | return "0.9.0-SNAPSHOT"; 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/config/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.config; 2 | 3 | import org.springframework.cache.CacheManager; 4 | import org.springframework.cache.annotation.CachingConfigurerSupport; 5 | import org.springframework.cache.annotation.EnableCaching; 6 | import org.springframework.cache.concurrent.ConcurrentMapCache; 7 | import org.springframework.cache.support.SimpleCacheManager; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import java.util.Arrays; 12 | 13 | /** 14 | * Created by 兰天 on 2015/4/11. 15 | */ 16 | @Configuration 17 | @EnableCaching 18 | public class CacheConfig extends CachingConfigurerSupport { 19 | 20 | @Bean 21 | @Override 22 | public CacheManager cacheManager() { 23 | SimpleCacheManager cacheManager = new SimpleCacheManager(); 24 | 25 | cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("avatars"), 26 | new ConcurrentMapCache("devices"), new ConcurrentMapCache("default"))); 27 | 28 | return cacheManager; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/config/JacksonMapperConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 6 | 7 | /** 8 | * Created by 兰天 on 2015/4/11. 9 | */ 10 | @Configuration 11 | public class JacksonMapperConfig { 12 | @Bean 13 | public Jackson2ObjectMapperBuilder jacksonBuilder() { 14 | Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); 15 | builder.indentOutput(true); 16 | return builder; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.io.ResourceLoader; 8 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 9 | import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 11 | 12 | import java.io.IOException; 13 | 14 | @Configuration 15 | public class WebConfig extends WebMvcConfigurerAdapter { 16 | 17 | private static final Logger logger = LoggerFactory.getLogger(WebConfig.class); 18 | 19 | private static final String[] SERVLET_RESOURCE_LOCATIONS = {"/"}; 20 | 21 | private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 22 | "classpath:/app/"}; 23 | 24 | private static final String[] RESOURCE_LOCATIONS; 25 | 26 | static { 27 | RESOURCE_LOCATIONS = new String[CLASSPATH_RESOURCE_LOCATIONS.length 28 | + SERVLET_RESOURCE_LOCATIONS.length]; 29 | System.arraycopy(SERVLET_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 0, 30 | SERVLET_RESOURCE_LOCATIONS.length); 31 | System.arraycopy(CLASSPATH_RESOURCE_LOCATIONS, 0, RESOURCE_LOCATIONS, 32 | SERVLET_RESOURCE_LOCATIONS.length, CLASSPATH_RESOURCE_LOCATIONS.length); 33 | } 34 | 35 | private static final String[] STATIC_INDEX_HTML_RESOURCES; 36 | 37 | static { 38 | STATIC_INDEX_HTML_RESOURCES = new String[RESOURCE_LOCATIONS.length]; 39 | for (int i = 0; i < STATIC_INDEX_HTML_RESOURCES.length; i++) { 40 | STATIC_INDEX_HTML_RESOURCES[i] = RESOURCE_LOCATIONS[i] + "index.html"; 41 | } 42 | } 43 | 44 | @Autowired 45 | private ResourceLoader resourceLoader; 46 | 47 | 48 | @Override 49 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 50 | registry.addResourceHandler("/**") 51 | .addResourceLocations(RESOURCE_LOCATIONS) 52 | .setCachePeriod(0); 53 | } 54 | 55 | @Override 56 | public void addViewControllers(ViewControllerRegistry registry) { 57 | addStaticIndexHtmlViewControllers(registry); 58 | } 59 | 60 | private void addStaticIndexHtmlViewControllers(ViewControllerRegistry registry) { 61 | for (String resource : STATIC_INDEX_HTML_RESOURCES) { 62 | if (this.resourceLoader.getResource(resource).exists()) { 63 | try { 64 | logger.info("Adding welcome page: " 65 | + this.resourceLoader.getResource(resource).getURL()); 66 | } catch (IOException ex) { 67 | // Ignore 68 | } 69 | // Use forward: prefix so that no view resolution is done 70 | registry.addViewController("/").setViewName("forward:index.html"); 71 | return; 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/controller/DeviceController.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.controller; 2 | 3 | import com.github.cosysoft.device.node.domain.Result; 4 | import com.github.cosysoft.device.node.service.DeviceService; 5 | import com.github.cosysoft.device.node.domain.Device; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.Collection; 13 | 14 | /** 15 | * Created by 兰天 on 2015/4/8. 16 | */ 17 | @RestController 18 | @RequestMapping("/api/device") 19 | public class DeviceController { 20 | 21 | @Autowired 22 | DeviceService deviceService; 23 | 24 | @RequestMapping 25 | public Collection devices() { 26 | return deviceService.getDevices(); 27 | } 28 | 29 | @RequestMapping(value = "/{serialId}/avatar", produces = MediaType.IMAGE_PNG_VALUE) 30 | public byte[] avatar(@PathVariable String serialId) { 31 | return deviceService.getAvatar(serialId); 32 | } 33 | 34 | @RequestMapping(value = "/{serialId}/screenshot", produces = MediaType.IMAGE_PNG_VALUE) 35 | public byte[] screenshot(@PathVariable String serialId) { 36 | return deviceService.takeScreenShot(serialId); 37 | } 38 | 39 | @RequestMapping(value = "/{serialId}/adb/{cmd}") 40 | public Result executeADB(@PathVariable String serialId, @PathVariable String cmd) { 41 | return deviceService.runAdbCommand(serialId, cmd); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/controller/MasterController.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.controller; 2 | 3 | import com.github.cosysoft.device.node.domain.Device; 4 | import com.github.cosysoft.device.node.service.MasterDeviceService; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.http.HttpEntity; 9 | import org.springframework.http.HttpMethod; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.stereotype.Controller; 12 | import org.springframework.web.bind.annotation.*; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | import javax.servlet.http.HttpServletRequest; 16 | import java.net.URI; 17 | import java.net.URISyntaxException; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | import java.util.regex.Pattern; 21 | 22 | /** 23 | * Created by 兰天 on 2015/4/13. 24 | */ 25 | @Controller 26 | @RequestMapping("/hub") 27 | public class MasterController { 28 | 29 | private static final Logger LOG = LoggerFactory.getLogger(MasterController.class); 30 | private static final List byteUrls = Arrays.asList("/avatar", "/screenshot"); 31 | 32 | private RestTemplate restTemplate = new RestTemplate(); 33 | 34 | @Autowired 35 | private MasterDeviceService deviceService; 36 | 37 | static final Pattern DIDRex = Pattern.compile("/hub/device/.*@.*/"); 38 | 39 | @RequestMapping(value = "/device/{serialId}/**") 40 | public ResponseEntity mirrorRest(@RequestBody(required = false) String body, HttpMethod method, 41 | HttpServletRequest request, @PathVariable String serialId) throws URISyntaxException { 42 | 43 | Device device = deviceService.getDeviceByDid(serialId); 44 | String path = DIDRex.matcher(request.getRequestURI()).replaceFirst("/api/device/" + device.getSerial() + "/"); 45 | 46 | URI uri = new URI("http", null, device.getNodeIP(), device.getNodePort(), 47 | path, request.getQueryString(), null); 48 | LOG.debug("redirect to {}", uri); 49 | 50 | Class responseType = String.class; 51 | for (String part : byteUrls) { 52 | if (request.getRequestURI().contains(part)) { 53 | responseType = byte[].class; 54 | break; 55 | } 56 | } 57 | ResponseEntity responseEntity = 58 | restTemplate.exchange(uri, method, new HttpEntity(body), responseType); 59 | 60 | return responseEntity; 61 | } 62 | 63 | @RequestMapping(value = "/device") 64 | @ResponseBody 65 | public List devices() { 66 | return deviceService.getDevices(); 67 | } 68 | 69 | @RequestMapping(value = "/register", method = {RequestMethod.POST}) 70 | @ResponseBody 71 | public String register(@RequestBody List devices) { 72 | deviceService.putOrDelete(devices); 73 | return "success"; 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/domain/Device.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.domain; 2 | 3 | import com.github.cosysoft.device.model.DeviceInfo; 4 | 5 | import java.util.Date; 6 | 7 | /** 8 | * Created by ltyao on 2015/4/14. 9 | */ 10 | public class Device extends DeviceInfo { 11 | 12 | private String nodeIP; 13 | private int nodePort; 14 | private Date lastRegisterDate = new Date(); 15 | 16 | 17 | public String getNodeIP() { 18 | return nodeIP; 19 | } 20 | 21 | public void setNodeIP(String nodeIP) { 22 | this.nodeIP = nodeIP; 23 | } 24 | 25 | public int getNodePort() { 26 | return nodePort; 27 | } 28 | 29 | public void setNodePort(int nodePort) { 30 | this.nodePort = nodePort; 31 | } 32 | 33 | public Date getLastRegisterDate() { 34 | return lastRegisterDate; 35 | } 36 | 37 | public void setLastRegisterDate(Date lastRegisterDate) { 38 | this.lastRegisterDate = lastRegisterDate; 39 | } 40 | 41 | public String getAvatarUri() { 42 | return "hub/device/" + this.getDid() + "/avatar"; 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | if (!super.equals(o)) return false; 50 | 51 | Device device = (Device) o; 52 | 53 | return getNodeIP().equals(device.getNodeIP()); 54 | 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | int result = super.hashCode(); 60 | result = 31 * result + getNodeIP().hashCode(); 61 | return result; 62 | } 63 | 64 | public String getDid() { 65 | return this.getSerial() + "@" + this.getNodeIP(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/domain/Result.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.domain; 2 | 3 | /** 4 | * Created by 兰天 on 2015/4/9. 5 | */ 6 | public class Result { 7 | 8 | private int code = 0; //0 is success 9 | private String message; 10 | private T payload; 11 | 12 | public int getCode() { 13 | return code; 14 | } 15 | 16 | public void setCode(int code) { 17 | this.code = code; 18 | } 19 | 20 | public String getMessage() { 21 | return message; 22 | } 23 | 24 | public void setMessage(String message) { 25 | this.message = message; 26 | } 27 | 28 | 29 | public T getPayload() { 30 | return payload; 31 | } 32 | 33 | public void setPayload(T payload) { 34 | this.payload = payload; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return "Result{" + 40 | "code=" + code + 41 | ", message='" + message + '\'' + 42 | ", payload=" + payload + 43 | '}'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/service/DeviceService.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.service; 2 | 3 | import com.github.cosysoft.device.node.domain.Result; 4 | import com.github.cosysoft.device.DeviceStore; 5 | import com.github.cosysoft.device.android.AndroidDevice; 6 | import com.github.cosysoft.device.android.impl.AndroidDeviceStore; 7 | import com.github.cosysoft.device.image.ImageUtils; 8 | import com.github.cosysoft.device.model.DeviceInfo; 9 | import com.github.cosysoft.device.node.domain.Device; 10 | import org.springframework.beans.BeanUtils; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.cache.annotation.Cacheable; 13 | import org.springframework.stereotype.Service; 14 | 15 | import java.awt.image.BufferedImage; 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | 19 | /** 20 | * Created by 兰天 on 2015/4/8. 21 | */ 22 | @Service 23 | public class DeviceService { 24 | 25 | private DeviceStore deviceStore = AndroidDeviceStore.getInstance(); 26 | 27 | @Autowired 28 | NodeService nodeService; 29 | 30 | @Cacheable(value = "avatars") 31 | public byte[] getAvatar(String serialId) { 32 | AndroidDevice device = deviceStore.getDeviceBySerial(serialId); 33 | BufferedImage image = device.takeScreenshot(); 34 | return ImageUtils.toByteArray(image); 35 | } 36 | 37 | public byte[] takeScreenShot(String serialId) { 38 | AndroidDevice device = deviceStore.getDeviceBySerial(serialId); 39 | BufferedImage image = device.takeScreenshot(); 40 | return ImageUtils.toByteArray(image); 41 | } 42 | 43 | public List getDevices() { 44 | List devices = new ArrayList<>(); 45 | for (AndroidDevice device : deviceStore.getDevices()) { 46 | Device deviceExtInfo = new Device(); 47 | DeviceInfo deviceInfo = device.getDeviceInfo(); 48 | BeanUtils.copyProperties(deviceInfo, deviceExtInfo); 49 | deviceExtInfo.setNodeIP(nodeService.getIp()); 50 | deviceExtInfo.setNodePort(nodeService.getPort()); 51 | 52 | devices.add(deviceExtInfo); 53 | } 54 | return devices; 55 | } 56 | 57 | public Result runAdbCommand(String serialId, String cmd) { 58 | Result result = new Result<>(); 59 | AndroidDevice device = deviceStore.getDeviceBySerial(serialId); 60 | String out = device.runAdbCommand(cmd); 61 | result.setPayload(out); 62 | return result; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/service/MasterDeviceService.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.service; 2 | 3 | import com.github.cosysoft.device.exception.DeviceNotFoundException; 4 | import com.github.cosysoft.device.node.domain.Device; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.List; 8 | import java.util.concurrent.CopyOnWriteArrayList; 9 | 10 | /** 11 | * Created by ltyao on 2015/4/14. 12 | */ 13 | @Service 14 | public class MasterDeviceService { 15 | 16 | private List devices = new CopyOnWriteArrayList<>(); 17 | 18 | public List getDevices() { 19 | return devices; 20 | } 21 | 22 | public Device getDeviceByDid(String did) { 23 | for (Device device : devices) { 24 | if (device.getDid().equals(did)) { 25 | return device; 26 | } 27 | } 28 | throw new DeviceNotFoundException(String.format("with did %s", did)); 29 | } 30 | 31 | public void putOrDelete(List ds) { 32 | if (ds.size() < 1) { 33 | return; 34 | } 35 | String ip = ds.get(0).getNodeIP(); 36 | for (Device device : devices) { 37 | if (device.getNodeIP().equals(ip)) { 38 | devices.remove(device); //safe for COW 39 | } 40 | } 41 | devices.addAll(ds); 42 | } 43 | 44 | public void deleteStale() { 45 | long current = System.currentTimeMillis(); 46 | for (Device device : devices) { 47 | if (current - device.getLastRegisterDate().getTime() > 3 * 1000) { 48 | devices.remove(device); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/service/NodeService.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.service; 2 | 3 | import com.github.cosysoft.device.exception.NestedException; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.net.InetAddress; 8 | import java.net.UnknownHostException; 9 | 10 | /** 11 | * Created by ltyao on 2015/4/14. 12 | */ 13 | @Service 14 | public class NodeService { 15 | 16 | @Value("${server.port:8080}") 17 | private int port; 18 | 19 | public String getIp() { 20 | try { 21 | return InetAddress.getLocalHost().getHostAddress(); 22 | } catch (UnknownHostException e) { 23 | throw new NestedException("", e); 24 | } 25 | } 26 | 27 | public String getName() { 28 | try { 29 | return InetAddress.getLocalHost().getHostName(); 30 | } catch (UnknownHostException e) { 31 | throw new NestedException("", e); 32 | } 33 | } 34 | 35 | public int getPort() { 36 | return port; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/task/DriveStaleDevice.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.task; 2 | 3 | import com.github.cosysoft.device.node.service.MasterDeviceService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.stereotype.Service; 7 | 8 | /** 9 | * Created by 兰天 on 2015/4/16. 10 | */ 11 | @Service 12 | public class DriveStaleDevice { 13 | 14 | @Autowired 15 | private MasterDeviceService deviceService; 16 | 17 | @Scheduled(fixedDelay = 3000) 18 | public void action() { 19 | deviceService.deleteStale(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /device-node/src/main/java/com/github/cosysoft/device/node/task/NodeRegister.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.task; 2 | 3 | import com.github.cosysoft.device.node.service.DeviceService; 4 | import com.github.cosysoft.device.node.domain.Device; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.scheduling.annotation.Scheduled; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.client.RestTemplate; 12 | 13 | import java.util.List; 14 | 15 | 16 | /** 17 | * Created by 兰天 on 2015/4/8. 18 | */ 19 | 20 | @Service 21 | public class NodeRegister { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(NodeRegister.class); 24 | 25 | @Autowired 26 | private DeviceService deviceService; 27 | 28 | private RestTemplate restTemplate = new RestTemplate(); 29 | 30 | 31 | @Value("${keeper.register}") 32 | private String registerUrl; 33 | 34 | @Scheduled(fixedDelay = 1000) 35 | public void register() { 36 | 37 | List devices = deviceService.getDevices(); 38 | LOG.info("register to {} with devices{}", registerUrl, devices); 39 | restTemplate.postForObject(registerUrl, devices, String.class); 40 | 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /device-node/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | keeper.register= http://localhost:8080/keeper/hub/register 2 | 3 | 4 | 5 | management.context-path=/manage 6 | 7 | server.tomcat.compression=off 8 | server.context-path=/keeper 9 | 10 | logging.level.org.cosysoft.device.*=DEBUG -------------------------------------------------------------------------------- /device-node/src/test/java/com/github/cosysoft/device/node/test/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.cosysoft.device.node.test; 2 | 3 | import com.github.cosysoft.device.node.domain.Device; 4 | import java.util.Map; 5 | import org.junit.Test; 6 | import org.springframework.boot.test.TestRestTemplate; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.core.StringContains.containsString; 14 | 15 | /** 16 | * Created by 兰天 on 2015/4/5. 17 | */ 18 | public class ApplicationTest { 19 | 20 | RestTemplate template = new TestRestTemplate(); 21 | 22 | @Test 23 | public void testVersion() { 24 | String version = template.getForEntity("http://localhost:8080/api/version", String.class).getBody(); 25 | assertThat(version, containsString("SNAPSHOT")); 26 | } 27 | 28 | @Test 29 | public void testRegister() { 30 | List devices = new ArrayList<>(); 31 | Device device = new Device(); 32 | device.setName("android-emulator"); 33 | devices.add(device); 34 | 35 | template.postForObject("http://localhost:8080/hub/register", devices, String.class); 36 | 37 | } 38 | 39 | @Test 40 | public void testPath() { 41 | 42 | Map envs = System.getenv(); 43 | envs.forEach((k, v) -> System.out.println(k + ":" + v)); 44 | } 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | systemProp.file.encoding=utf-8 2 | -------------------------------------------------------------------------------- /gradle/publishInternalNexus.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | 3 | publishing { 4 | publications { 5 | mavenJava(MavenPublication) { 6 | from components.java 7 | 8 | artifact sourcesJar { 9 | classifier "sources" 10 | } 11 | 12 | artifact javadocJar { 13 | classifier "javadoc" 14 | } 15 | } 16 | } 17 | repositories { 18 | maven { 19 | credentials { 20 | username inexusUser 21 | password inexusPassword 22 | } 23 | 24 | if (project.version.endsWith('-SNAPSHOT')) { 25 | url inexusSnapshotRepo 26 | } else { 27 | url inexusReleaseRepo 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /gradle/upload.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'signing' 2 | 3 | 4 | signing { 5 | sign configurations.archives 6 | } 7 | 8 | uploadArchives { 9 | repositories { 10 | mavenDeployer { 11 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 12 | 13 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 14 | authentication(userName: ossrhUsername, password: ossrhPassword) 15 | } 16 | 17 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 18 | authentication(userName: ossrhUsername, password: ossrhPassword) 19 | } 20 | pom.project { 21 | name 'ddmlib-facade' 22 | packaging 'jar' 23 | description 'A set of tools for operate android device via android debug bridge' 24 | url 'https://github.com/cosysoft/device' 25 | 26 | scm { 27 | connection 'scm:git:http://github.com/cosysoft/device.git' 28 | developerConnection 'scm:git:ssh://github.com:cosysoft/device.git' 29 | url 'https://github.com/cosysoft/device' 30 | } 31 | 32 | licenses { 33 | license { 34 | name 'The Apache License, Version 2.0' 35 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 36 | } 37 | } 38 | 39 | developers { 40 | developer { 41 | id 'cosyman' 42 | name 'Bluesky Yao' 43 | email 'cosyman@outlook.com' 44 | } 45 | } 46 | } 47 | 48 | 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'device' 2 | 3 | include 'device-api' 4 | include 'device-node' 5 | 6 | --------------------------------------------------------------------------------