├── icon.png ├── icon_large.png ├── css ├── app_icon.png ├── fonts │ ├── icomoon.ttf │ └── Roboto-Regular.ttf ├── auto-complete.css ├── icons.css ├── app.css ├── dlist.css ├── components.css └── hview.css ├── commands ├── methods.jar ├── inputserver.jar ├── processicon.jar ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ ├── MethodList.java │ ├── ProcessIcon.java │ ├── InputServer.java │ └── DisplayServer.java ├── proxy ├── webhv-proxy.jar └── build.gradle ├── .gitignore ├── manifest.json ├── README.md ├── third_party ├── pako │ └── LICENSE └── auto-complete.min.js ├── CONTRIBUTING.md ├── js ├── constants.js ├── back_stack.js ├── ddmlib │ ├── DataOutputStream.js │ ├── DataInputStream.js │ ├── worker.js │ ├── tl-worker.js │ ├── parser_v1.js │ ├── viewnode.js │ ├── jdwp.js │ ├── parser_v2.js │ └── property_formatter.js ├── adb │ ├── adb_proxy.js │ ├── adb_common.js │ ├── adb_msg.js │ ├── crypto.js │ └── adb.js ├── diff.js ├── utils.js ├── activity_list.js ├── file_load_worker.js ├── index.js └── dmirror.js ├── protos ├── view_capture_multi_window.proto └── view_capture_magic_number.proto ├── index.html └── LICENSE /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/icon.png -------------------------------------------------------------------------------- /icon_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/icon_large.png -------------------------------------------------------------------------------- /css/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/css/app_icon.png -------------------------------------------------------------------------------- /commands/methods.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/commands/methods.jar -------------------------------------------------------------------------------- /css/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/css/fonts/icomoon.ttf -------------------------------------------------------------------------------- /proxy/webhv-proxy.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/proxy/webhv-proxy.jar -------------------------------------------------------------------------------- /commands/inputserver.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/commands/inputserver.jar -------------------------------------------------------------------------------- /commands/processicon.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/commands/processicon.jar -------------------------------------------------------------------------------- /css/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/web-hv/HEAD/css/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.gradle/* 2 | **/gradle/* 3 | **/gradlew* 4 | **/local.properties 5 | **/bin/** 6 | **/.idea/* 7 | 8 | **/*.iml 9 | **/.DS_Store 10 | **/build/* 11 | .vscode/* -------------------------------------------------------------------------------- /commands/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | gradle.properties 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Web Hierarchy Viewer", 3 | "short_name": "Web HV", 4 | "display": "fullscreen", 5 | "start_url": "index.html", 6 | "theme_color": "#4CAF50", 7 | "background_color": "#f2f2f2", 8 | "icons": [ 9 | { 10 | "src": "icon.png", 11 | "sizes": "192x192", 12 | "type": "image/png" 13 | }, 14 | { 15 | "src": "icon_large.png", 16 | "sizes": "512x512", 17 | "type": "image/png" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /proxy/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | sourceSets { 4 | main { 5 | java { 6 | srcDir "src" 7 | } 8 | } 9 | } 10 | 11 | //create a single Jar with all dependencies 12 | task fatJar(type: Jar) { 13 | manifest { 14 | attributes 'Main-Class': 'com.webhv.ProxyServer' 15 | } 16 | archiveBaseName = 'webhv-proxy' 17 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 18 | from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } 19 | with jar 20 | } 21 | -------------------------------------------------------------------------------- /commands/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | google() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:8.3.2' 8 | } 9 | } 10 | 11 | apply plugin: 'com.android.library' 12 | 13 | android { 14 | namespace 'com.google.tools.webhv' 15 | compileSdkVersion "android-UmainForTreecae3" 16 | 17 | defaultConfig { 18 | minSdk 24 19 | targetSdk 34 20 | versionCode 1 21 | versionName "1.0" 22 | } 23 | 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | allprojects { 31 | repositories { 32 | mavenCentral() 33 | google() 34 | jcenter() 35 | } 36 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Hierarchy Viewer 2 | 3 | A web based tool for inspecting UI of an in-development android app. [Launch App](https://google.github.io/web-hv) 4 | 5 | ## Development 6 | To run the project locally, checkout the code and start a [local web server](https://gist.github.com/willurd/5720255) in the root directory. This tool only uses HTML5/javascript APIs and doesn't have any server side component. 7 | 8 | See CONTRIBUTING.md on how to submit patches. 9 | 10 | ## Dependencies 11 | - [JQuery](https://github.com/jquery/jquery) 12 | - [JavaScript-autoComplete](https://github.com/Pixabay/JavaScript-autoComplete) 13 | - [JSZip](http://stuartk.com/jszip) 14 | - [pako](https://github.com/nodeca/pako) 15 | - [jsbn](http://www-cs-students.stanford.edu/~tjw/jsbn/) 16 | 17 | ## Features 18 | - Inspect ui of running apps (debug apps) 19 | - Run commands on view nodes and change properties at runtime 20 | - Save hierarcy to disk for offline viewing and sharing 21 | 22 | 23 | This is not an officially supported Google product 24 | -------------------------------------------------------------------------------- /third_party/pako/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /css/auto-complete.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 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 | .autocomplete-suggestions { 18 | text-align: left; 19 | cursor: default; 20 | border-top: 0; 21 | background: var(--window-bg-color); 22 | box-shadow: 0 2px 4px rgba(0,0,0,0.2); 23 | 24 | /* core styles should not be changed */ 25 | position: absolute; display: none; z-index: 9999; max-height: 254px; overflow: hidden; overflow-y: auto; box-sizing: border-box; 26 | } 27 | .autocomplete-suggestion { 28 | position: relative; 29 | padding: 0 15px; 30 | line-height: 27px; 31 | white-space: nowrap; 32 | overflow: hidden; 33 | text-overflow: ellipsis; 34 | font-size: 13px; 35 | } 36 | .autocomplete-suggestion b { 37 | font-weight: normal; 38 | color: var(--filter-color); 39 | } 40 | .autocomplete-suggestion.selected { 41 | background: var(--hover-bg-color); 42 | } 43 | -------------------------------------------------------------------------------- /js/constants.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const CLS_EXPANDABLE = "expandable"; 16 | const CLS_CLOSED = "closed"; 17 | const CLS_TREENODE = "treenode"; 18 | const CLS_SELECTED = "selected"; 19 | const CLS_LAST_SELECTED = "last_selected"; 20 | const CLS_HOVER = "hover"; 21 | const CLS_FORCE_NO_BG = "force-no-bg"; 22 | const CLS_HIDE_MY_BG = "hide-my-bg"; 23 | const CLS_DISABLED = "disabled"; 24 | const CLS_WITH_ARROW = "with_arrow"; 25 | const CLS_MULTI_TOGGLE = "multi-toggle" 26 | const CLS_COLORWELL = "colorwell"; 27 | 28 | const URL_LOADING = "_loading_"; 29 | 30 | const TYPE_ERROR = -1; 31 | const TYPE_ZIP = 0; 32 | const TYPE_OLD = 1; 33 | const TYPE_JDWP = 2; 34 | const TYPE_BUG_REPORT = 3; 35 | const TYPE_BUG_REPORT_V2 = 4; // Bug report with encoded view hierarchy 36 | const TYPE_TIME_LAPSE_BUG_REPORT = 5; 37 | 38 | const CMD_CONVERT_TO_STRING = 1; 39 | const CMD_PARSE_OLD_DATA = 2; 40 | const CMD_USE_PROPERTY_MAP = 4; 41 | const CMD_DEFLATE_STRING = 8; 42 | const CMD_SKIP_8_BITS = 16; 43 | 44 | const VIEW_VISIBLE = 0; 45 | const VIEW_CAPTURE_REGEX = /.*\.vc/ 46 | const WM_TRACE_DIR = "FS/data/misc/wmtrace" 47 | 48 | const TL_ACTION_UNWRAP = 0; 49 | const TL_ACTION_LOAD_WINDOW = 1; -------------------------------------------------------------------------------- /css/icons.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 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 | @font-face { 18 | font-family: "Icon-Font"; 19 | src: url("fonts/icomoon.ttf"); 20 | font-weight: normal; 21 | font-style: normal; 22 | } 23 | 24 | .icon_btn::before { 25 | font-family: "Icon-Font"; 26 | } 27 | 28 | .ic_refresh::before { 29 | content: "\e5d5"; 30 | } 31 | .ic_search::before { 32 | content: "\e8b6"; 33 | } 34 | .ic_options::before { 35 | content: "\e5d4"; /* more_vert */ 36 | } 37 | .ic_save::before { 38 | content: "\e161"; 39 | } 40 | .ic_hide::before { 41 | content: "\e8f5"; 42 | } 43 | .ic_show::before { 44 | content: "\e8f4"; 45 | } 46 | .ic_collapse::before { 47 | content: "\e5d6"; /* unfold_less */ 48 | } 49 | .ic_play::before { 50 | content: "\e037"; 51 | } 52 | .ic_layers::before { 53 | content: "\e53b"; 54 | } 55 | .ic_checked::before { 56 | content: "\e834"; 57 | } 58 | .ic_unchecked::before { 59 | content: "\e835"; 60 | } 61 | .ic_disconnect::before { 62 | content: "\e0db"; /* phonelink_erase */ 63 | } 64 | .ic_copy::before { 65 | content: "\e905"; 66 | } 67 | 68 | .ic_submenu::after { 69 | content: "\e5c5"; 70 | transform: rotate(-90deg); 71 | font-family: "Icon-Font"; 72 | float: right; 73 | line-height: 27px; 74 | font-size: 18px; 75 | } -------------------------------------------------------------------------------- /protos/view_capture_multi_window.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 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 | syntax = "proto2"; 18 | 19 | package com.android.app.viewcapture.data; 20 | 21 | option java_multiple_files = true; 22 | 23 | message ExportedData { 24 | repeated WindowData windowData = 1; 25 | optional string package = 2; 26 | repeated string classname = 3; 27 | } 28 | 29 | message WindowData { 30 | repeated FrameData frameData = 1; 31 | optional string title = 2; 32 | } 33 | 34 | message FrameData { 35 | optional int64 timestamp = 1; // choreographer timestamp in nanoseconds 36 | optional ViewNode node = 2; 37 | } 38 | 39 | message ViewNode { 40 | optional int32 classname_index = 1; 41 | optional int32 hashcode = 2; 42 | 43 | repeated ViewNode children = 3; 44 | 45 | optional string id = 4; 46 | optional int32 left = 5; 47 | optional int32 top = 6; 48 | optional int32 width = 7; 49 | optional int32 height = 8; 50 | optional int32 scrollX = 9; 51 | optional int32 scrollY = 10; 52 | 53 | optional float translationX = 11; 54 | optional float translationY = 12; 55 | optional float scaleX = 13 [default = 1]; 56 | optional float scaleY = 14 [default = 1]; 57 | optional float alpha = 15 [default = 1]; 58 | 59 | optional bool willNotDraw = 16; 60 | optional bool clipChildren = 17; 61 | optional int32 visibility = 18; 62 | 63 | optional float elevation = 19; 64 | } 65 | -------------------------------------------------------------------------------- /js/back_stack.js: -------------------------------------------------------------------------------- 1 | var backStack = (function() { 2 | 3 | $(function() { 4 | 5 | // Check url hash 6 | const urlParams = new URLSearchParams(window.location.search); 7 | let base = window.location.origin + window.location.pathname; 8 | if (urlParams.get("mode") == "mirror") { 9 | // Switch to mirror mode 10 | $("#main-title-wrapper").html("

Mirror android screen

"); 11 | activityListAction = deviceMirrorAction; 12 | base += "?mode=mirror"; 13 | } else if (urlParams.get("mode") == "secondary") { 14 | try { 15 | let width = parseInt(urlParams.get("width")) 16 | let height = parseInt(urlParams.get("height")) 17 | let dpi = parseInt(urlParams.get("dpi")) 18 | if (width <= 0 || isNaN(width) || height <= 0 || isNaN(height) || dpi <= 0|| isNaN(dpi)) { 19 | throw "Invalid device config" 20 | } 21 | let extendDisplay = new ExtendedDisplay(width, height, dpi) 22 | 23 | // Switch to mirror mode 24 | $("#main-title-wrapper").html("

Android secondary display

"); 25 | activityListAction = () => deviceMirrorAction(extendDisplay); 26 | base += extendDisplay.toUrlParams(); 27 | } catch(e) { 28 | toast("Unable to start secondary display: " + e); 29 | } 30 | } 31 | 32 | history.replaceState({}, "", base); 33 | }); 34 | 35 | var cbStack = []; 36 | 37 | window.onpopstate = function(event) { 38 | cbStack.pop(); 39 | if (cbStack.length > 0) { 40 | let last = cbStack[cbStack.length - 1]; 41 | if (last[0] == event.state.url) { 42 | console.log("Found"); 43 | last[1](); 44 | return; 45 | } 46 | } 47 | location.reload(); 48 | } 49 | 50 | return { 51 | 52 | add: function(url, callback) { 53 | cbStack.push([url, callback]); 54 | history.pushState({url: url}, "", url); 55 | } 56 | }; 57 | })(); -------------------------------------------------------------------------------- /commands/src/main/java/MethodList.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import android.view.View; 16 | 17 | import org.json.JSONArray; 18 | 19 | import java.lang.reflect.Method; 20 | 21 | /** 22 | * A simple program to dump possible methods defined in {@link View} 23 | * that can be called using DDMS protocol. 24 | */ 25 | public class MethodList { 26 | 27 | public static void main(String[] args) { 28 | JSONArray out = new JSONArray(); 29 | for (Method m : View.class.getMethods()) { 30 | if (m.getName().startsWith("get") || m.getName().startsWith("on") || m.getName().startsWith("is")) { 31 | continue; 32 | } 33 | boolean valid = true; 34 | String params = ""; 35 | for (Class c : m.getParameterTypes()) { 36 | if (!params.isEmpty()) { 37 | params += ", "; 38 | } 39 | if (c == int.class) { 40 | params += "int"; 41 | } else if (c == float.class) { 42 | params += "float"; 43 | } else if (c == boolean.class) { 44 | params += "boolean"; 45 | } else { 46 | valid = false; 47 | break; 48 | } 49 | } 50 | if (valid) { 51 | out.put(new JSONArray().put(m.getName()).put(params)); 52 | } 53 | } 54 | // Everything initialized. Send OKAY. 55 | System.out.println("OKAY"); 56 | System.out.println(out); 57 | } 58 | } -------------------------------------------------------------------------------- /protos/view_capture_magic_number.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2023 The Android Open Source Project 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 | syntax = "proto2"; 18 | 19 | package com.android.app.viewcapture.data; 20 | 21 | option java_multiple_files = true; 22 | 23 | message ExportedData { 24 | /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | MagicNumber.MAGIC_NUMBER_L 25 | (this is needed because enums have to be 32 bits and there's no nice way to put 64bit 26 | constants into .proto files. */ 27 | enum MagicNumber { 28 | INVALID = 0; 29 | MAGIC_NUMBER_L = 0x65906578; /* AZAN (ASCII) */ 30 | MAGIC_NUMBER_H = 0x68658273; /* DARI (ASCII) */ 31 | } 32 | 33 | optional fixed64 magic_number = 1; /* Must be the first field, set to value in MagicNumber */ 34 | repeated WindowData windowData = 2; 35 | optional string package = 3; 36 | repeated string classname = 4; 37 | } 38 | 39 | message WindowData { 40 | repeated FrameData frameData = 1; 41 | optional string title = 2; 42 | } 43 | 44 | message FrameData { 45 | optional int64 timestamp = 1; // choreographer timestamp in nanoseconds 46 | optional ViewNode node = 2; 47 | } 48 | 49 | message ViewNode { 50 | optional int32 classname_index = 1; 51 | optional int32 hashcode = 2; 52 | 53 | repeated ViewNode children = 3; 54 | 55 | optional string id = 4; 56 | optional int32 left = 5; 57 | optional int32 top = 6; 58 | optional int32 width = 7; 59 | optional int32 height = 8; 60 | optional int32 scrollX = 9; 61 | optional int32 scrollY = 10; 62 | 63 | optional float translationX = 11; 64 | optional float translationY = 12; 65 | optional float scaleX = 13 [default = 1]; 66 | optional float scaleY = 14 [default = 1]; 67 | optional float alpha = 15 [default = 1]; 68 | 69 | optional bool willNotDraw = 16; 70 | optional bool clipChildren = 17; 71 | optional int32 visibility = 18; 72 | 73 | optional float elevation = 19; 74 | } 75 | -------------------------------------------------------------------------------- /js/ddmlib/DataOutputStream.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | function DataOutputStream() { 16 | this.data = []; 17 | this.highFirst = true; 18 | } 19 | 20 | DataOutputStream.prototype.intMax = Math.pow(2, 32); 21 | DataOutputStream.prototype.shortMax = Math.pow(2, 16); 22 | DataOutputStream.prototype.intSignedMax = Math.pow(2, 31); 23 | 24 | /** 25 | * @param byte byte to write 26 | * @param pos optional position otherwise, data is written to the end. 27 | */ 28 | DataOutputStream.prototype.writeByte = function(byte, pos) { 29 | if (pos == undefined) pos = this.data.length; 30 | this.data[pos] = byte & 0x00FF; 31 | } 32 | 33 | DataOutputStream.prototype.writeBytes = function(bytes, pos) { 34 | if (pos == undefined) pos = this.data.length; 35 | const length = bytes.length; 36 | for (let i = 0; i < length; i++, pos++) { 37 | this.data[pos] = bytes[i]; 38 | } 39 | } 40 | 41 | DataOutputStream.prototype.writeInt = function(number, pos) { 42 | if (this.highFirst) { 43 | this.writeBytes([((number & 0xFF000000) >> 24), ((number & 0x00FF0000) >> 16), ((number & 0xFF00) >> 8), (number & 0x00FF)], pos); 44 | } else { 45 | this.writeBytes([(number & 0x00FF), ((number & 0xFF00) >> 8), ((number & 0x00FF0000) >> 16), ((number & 0xFF000000) >> 24)], pos); 46 | } 47 | } 48 | 49 | DataOutputStream.prototype.writeFloat = function(number, pos) { 50 | let arr = new Float32Array(1); 51 | arr[0] = number; 52 | arr = new Int32Array(arr.buffer, arr.byteOffset); 53 | this.writeInt(arr[0]); 54 | } 55 | 56 | DataOutputStream.prototype.writeStr = function(str, doNotWriteLen) { 57 | const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char 58 | let bufView = new Uint16Array(buf); 59 | for (let i = 0; i < str.length; i++) { 60 | bufView[i] = str.charCodeAt(i); 61 | } 62 | bufView = new Uint8Array(buf); 63 | if (this.highFirst) { 64 | swapAltElements(bufView); 65 | } 66 | if (!doNotWriteLen) { 67 | this.writeInt(str.length); 68 | } 69 | this.writeBytes(bufView); 70 | } 71 | -------------------------------------------------------------------------------- /js/ddmlib/DataInputStream.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const swapAltElements = function(arr) { 16 | for (let i = 0; i < arr.length; i += 2) { 17 | const tmp = arr[i]; 18 | arr[i] = arr[i + 1]; 19 | arr[i + 1] = tmp; 20 | } 21 | } 22 | 23 | function DataInputStream(data) { 24 | this.data = data; 25 | this.highFirst = true; 26 | this.pos = 0; 27 | 28 | this._view = new DataView(data.buffer); 29 | } 30 | 31 | DataInputStream.prototype.intMax = Math.pow(2, 32); 32 | DataInputStream.prototype.shortMax = Math.pow(2, 16); 33 | DataInputStream.prototype.intSignedMax = Math.pow(2, 31); 34 | 35 | DataInputStream.prototype.read = function() { 36 | return this.data[this.pos++]; 37 | } 38 | 39 | DataInputStream.prototype.readInt = function() { 40 | const pos = this.pos; 41 | this.pos += 4; 42 | return this._view.getInt32(pos, !this.highFirst); 43 | } 44 | 45 | DataInputStream.prototype.readShort = function() { 46 | const pos = this.pos; 47 | this.pos += 2; 48 | return this._view.getInt16(pos, !this.highFirst); 49 | } 50 | 51 | DataInputStream.prototype.readFloat = function() { 52 | const pos = this.pos; 53 | this.pos += 4; 54 | return this._view.getFloat32(pos, !this.highFirst); 55 | } 56 | 57 | DataInputStream.prototype.readDouble = function() { 58 | const pos = this.pos; 59 | this.pos += 8; 60 | return this._view.getFloat64(pos, !this.highFirst); 61 | }; 62 | 63 | DataInputStream.prototype.readLong = function() { 64 | return this.readDouble(8); 65 | } 66 | 67 | 68 | DataInputStream.prototype.readStr = function(len) { 69 | if (len == undefined) { 70 | len = this.readInt(); 71 | } 72 | let slice = this.data.subarray(this.pos, this.pos += 2 * len); 73 | if (this.highFirst) { 74 | swapAltElements(slice); 75 | } 76 | slice = new Uint16Array(slice.buffer, slice.byteOffset, len); 77 | return String.fromCharCode.apply(null, slice); 78 | } 79 | 80 | DataInputStream.prototype.readStrSmall = function() { 81 | const len = this.readShort(); 82 | let slice = this.data.subarray(this.pos, this.pos += len); 83 | slice = new Uint8Array(slice.buffer, slice.byteOffset, len); 84 | return String.fromCharCode.apply(null, slice); 85 | } 86 | -------------------------------------------------------------------------------- /js/ddmlib/worker.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | importScripts("viewnode.js"); 16 | importScripts("property_formatter.js"); 17 | 18 | const CMD_CONVERT_TO_STRING = 1; 19 | const CMD_PARSE_OLD_DATA = 2; 20 | const CMD_USE_PROPERTY_MAP = 4; 21 | const CMD_DEFLATE_STRING = 8; 22 | const CMD_SKIP_8_BITS = 16; 23 | 24 | let commonProps = null; 25 | 26 | function convertToString(data) { 27 | const total = data.length; 28 | let result = ""; 29 | for (let st = 8; st < total; st++) { 30 | result += String.fromCharCode(data[st]); 31 | } 32 | return result; 33 | } 34 | 35 | self.onmessage = function(e) { 36 | const msg = e.data; 37 | const cmd = msg.cmd; 38 | let data = msg.data; 39 | 40 | let bitShift = 0; 41 | 42 | if ((cmd & CMD_DEFLATE_STRING) != 0) { 43 | importScripts("../../third_party/pako/pako_inflate.min.js"); 44 | data = pako.inflate(data); 45 | } 46 | 47 | if ((cmd & CMD_SKIP_8_BITS) != 0) { 48 | bitShift = 8; 49 | } 50 | 51 | if ((cmd & CMD_CONVERT_TO_STRING) != 0) { 52 | data = convertToString(data); 53 | } 54 | 55 | if ((cmd & CMD_USE_PROPERTY_MAP) != 0) { 56 | commonProps = { 57 | id : "mID", 58 | left: "mLeft", 59 | top: "mTop", 60 | width: "getWidth()", 61 | height: "getHeight()", 62 | scrollX: "mScrollX", 63 | scrollY: "mScrollY", 64 | willNotDraw: "willNotDraw()", 65 | clipChildren: "getClipChildren()", 66 | translationX: "getTranslationX()", 67 | translationY: "getTranslationY()", 68 | scaleX: "getScaleX()", 69 | scaleY: "getScaleY()", 70 | contentDescription: "getContentDescription()", 71 | text: "getText()", 72 | visibility: "getVisibility()" 73 | } 74 | } 75 | 76 | if ((cmd & CMD_PARSE_OLD_DATA) != 0) { 77 | importScripts("parser_v1.js"); 78 | } else { 79 | importScripts("DataInputStream.js"); 80 | importScripts("parser_v2.js"); 81 | } 82 | 83 | const rootNode = parseNode(data, bitShift); 84 | formatProperties(rootNode); 85 | 86 | postMessage({ 87 | viewHierarchyData: rootNode 88 | }); 89 | close(); 90 | } 91 | -------------------------------------------------------------------------------- /js/ddmlib/tl-worker.js: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | importScripts("property_formatter.js") 16 | importScripts("../../third_party/protobuf.min.js") 17 | importScripts("../../third_party/pako/pako_inflate.min.js") 18 | importScripts("viewnode.js") 19 | importScripts("../constants.js") 20 | 21 | let classNames 22 | let windowList 23 | let packageName 24 | 25 | self.onmessage = function(event) { 26 | if (event.data.action == TL_ACTION_UNWRAP) { 27 | const protoFileVersion = (hasMagicNumber(event.data.data)) 28 | ? "magic_number" 29 | : "multi_window" 30 | protobuf.load(`../../protos/view_capture_${protoFileVersion}.proto`).then(async function(root) { 31 | const exportedData = root 32 | .lookupType("com.android.app.viewcapture.data.ExportedData") 33 | .decode(event.data.data) 34 | classNames = exportedData.classname 35 | windowList = exportedData.windowData 36 | packageName = exportedData.package 37 | 38 | for (let i = 0; i < windowList.length; i++) { 39 | postMessage({ title: packageName + windowList[i].title, index: i}) 40 | } 41 | }) 42 | } else if (event.data.action == TL_ACTION_LOAD_WINDOW) { 43 | processFrames(windowList[event.data.index], classNames) 44 | } 45 | } 46 | 47 | const processFrames = function (frameListContainer, classNameList) { 48 | const rootNodes /* ViewNode[] */ = frameListContainer.frameData.map(f => f.node) 49 | postMessage({ frameCount: rootNodes.length }) 50 | 51 | for (let i = 0; i < rootNodes.length; i++) { 52 | formatProperties(rootNodes[i], classNameList) 53 | postMessage({ rootNode: rootNodes[i] }) 54 | } 55 | } 56 | 57 | const hasMagicNumber = function(uInt8Array) { 58 | const MAGIC_NUMBER = [0x9, 0x78, 0x65, 0x90, 0x65, 0x73, 0x82, 0x65, 0x68] 59 | 60 | const arrayEquals = function(one, other) { 61 | if (one.length !== other.length) { 62 | return false; 63 | } 64 | 65 | for (let i = 0; i < one.length; i++) { 66 | if (one[i] !== other[i]) { 67 | return false; 68 | } 69 | } 70 | 71 | return true; 72 | } 73 | 74 | return arrayEquals(MAGIC_NUMBER, uInt8Array.slice(0, MAGIC_NUMBER.length)) 75 | } -------------------------------------------------------------------------------- /js/adb/adb_proxy.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const WEB_PROXY_VERSION = 1; 16 | const STANDARD_ERROR_CODE = 4010; 17 | 18 | class WebProxyDevice extends BaseAdbDevice { 19 | 20 | constructor(device) { 21 | super() 22 | this.device = device; 23 | this.streams = []; 24 | this.nextLocalId = 5; 25 | } 26 | 27 | openStream(command) { 28 | var serialNumber = this.device.serial; 29 | const stream = new WebSocketStream(new WebSocket("ws://localhost:9167/adb-json")); 30 | const localId = this.nextLocalId++; 31 | this.streams[localId] = stream; 32 | stream.onReceiveCloseInternal = this.#clearStream.bind(this, localId) 33 | stream.write(JSON.stringify({ 34 | header: {serialNumber, command} 35 | })) 36 | return stream; 37 | } 38 | 39 | closeAll() { 40 | console.log("Closing all"); 41 | for (let i = 0; i < this.streams.length; i++) { 42 | if (this.streams[i]) { 43 | this.streams[i].onClose = null; 44 | this.streams[i].close(); 45 | } 46 | } 47 | } 48 | 49 | #clearStream(localId) { 50 | this.streams[localId] = undefined; 51 | } 52 | } 53 | 54 | class WebSocketStream extends BaseAdbStream { 55 | 56 | #isOpen = deferred() 57 | 58 | /** 59 | * @param {WebSocket} socket 60 | */ 61 | constructor(socket) { 62 | super() 63 | this.socket = socket; 64 | 65 | socket.binaryType = "arraybuffer" 66 | socket.onmessage = this.#onMessage.bind(this); 67 | socket.onclose = this.onReceiveClose.bind(this); 68 | socket.onerror = this.onReceiveClose.bind(this); 69 | socket.onopen = this.#isOpen.accept.bind(this.#isOpen, true); 70 | } 71 | 72 | close() { 73 | this.socket.close(); 74 | } 75 | 76 | async write(data) { 77 | await this.#isOpen; 78 | this.socket.send(data); 79 | } 80 | 81 | #onMessage(event) { 82 | // console.log("Data received:", event.data); 83 | // console.log(new TextDecoder("utf-8").decode(event.data)); 84 | this.onReceiveWrite(new Uint8Array(event.data)); 85 | } 86 | 87 | onReceiveClose() { 88 | this.onReceiveCloseInternal(); 89 | super.onReceiveClose() 90 | } 91 | 92 | onReceiveCloseInternal() { } 93 | } 94 | -------------------------------------------------------------------------------- /js/ddmlib/parser_v1.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | const countFrontWhitespace = function(line) { 17 | const m = line.match(/^\s+/); 18 | return m ? m[0].length : 0 19 | }; 20 | 21 | const loadProperties = function(node, data) { 22 | let start = 0; 23 | let stop; 24 | 25 | do { 26 | const index = data.indexOf('=', start); 27 | const property = new VN_Property(data.substring(start, index)); 28 | 29 | const index2 = data.indexOf(',', index + 1); 30 | const length = parseInt(data.substring(index + 1, index2)); 31 | start = index2 + 1 + length; 32 | property.value = data.substring(index2 + 1, index2 + 1 + length); 33 | 34 | node.properties.push(property); 35 | node.namedProperties[property.name] = property; 36 | 37 | stop = start >= data.length; 38 | if (!stop) { 39 | start += 1; 40 | } 41 | } while (!stop); 42 | 43 | node.sortProperties(); 44 | node.loadCommonProperties(commonProps); 45 | } 46 | 47 | /** 48 | * Parses the view node data and returns the root node 49 | */ 50 | const parseNode = function(data) { 51 | const stack = []; 52 | let root = null; 53 | let lastNode = null; 54 | let lastWhitespaceCount = -INT_MIN_VALUE; 55 | data = data.split("\n"); 56 | for (let l = 0; l < data.length - 1; l++) { 57 | let line = data[l]; 58 | if (line.toUpperCase() == "DONE.") { 59 | break; 60 | } 61 | 62 | const whitespaceCount = countFrontWhitespace(line); 63 | if (lastWhitespaceCount < whitespaceCount) { 64 | stack.push(lastNode); 65 | } else if (stack.length) { 66 | const count = lastWhitespaceCount - whitespaceCount; 67 | for (let i = 0; i < count; i++) { 68 | stack.pop(); 69 | } 70 | } 71 | 72 | lastWhitespaceCount = whitespaceCount; 73 | line = line.trim(); 74 | const index = line.indexOf(' '); 75 | lastNode = new ViewNode(line.substring(0, index)); 76 | 77 | line = line.substring(index + 1); 78 | loadProperties(lastNode, line); 79 | 80 | if (!root) { 81 | root = lastNode; 82 | } 83 | 84 | if (stack.length) { 85 | const parent = stack[stack.length - 1]; 86 | parent.children.push(lastNode); 87 | } 88 | } 89 | 90 | return root; 91 | } -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 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 | @font-face { 18 | font-family: "Roboto"; 19 | src: url("fonts/Roboto-Regular.ttf"); 20 | font-weight: 400; 21 | } 22 | 23 | :root { 24 | --title-size: 0px; 25 | 26 | /* App Theme colors */ 27 | --title-color: #2196F3; 28 | --progress-color: #1976D2; 29 | 30 | --selected-color: #3b99fc; 31 | --last-selected-color: rgba(21,135,215,0.50); 32 | --under-cursor-color: #44aed8; 33 | 34 | --filter-color: #E65100; 35 | --error-color: #F44336; 36 | 37 | /* Light and dark theme */ 38 | --window-bg-color: #f2f2f2; 39 | --card-bg-color: #f9f9f9; 40 | --button-bg-color: #F0f0f0; 41 | --hover-bg-color: rgba(0, 0, 0, 0.05); 42 | 43 | --divider-color: #cccccc; 44 | 45 | --text-color: #333; 46 | --subtext-color: #999999; 47 | --title-text-color: #263238; 48 | } 49 | 50 | :root { 51 | --clear-icon: url("data:image/svg+xml;utf8,"); 52 | } 53 | 54 | body.darkTheme { 55 | --window-bg-color: #222222; 56 | --card-bg-color: #424242; 57 | --button-bg-color: #666; 58 | --hover-bg-color: rgba(255, 255, 255, 0.1); 59 | --divider-color: #666; 60 | 61 | --text-color: #e5e5e5; 62 | --subtext-color: #aaa; 63 | --title-text-color: #FFF; 64 | } 65 | 66 | body { 67 | margin: 0; 68 | padding: 0; 69 | background: var(--window-bg-color); 70 | font-family: Roboto, 'Open Sans', sans-serif; 71 | color: var(--text-color); 72 | font-size: 14px; 73 | } 74 | 75 | #start-screen { 76 | padding: 40px; 77 | text-align: center; 78 | } 79 | 80 | #start-screen button { 81 | font-size: 20px; 82 | line-height: 60px; 83 | height: 60px; 84 | display: block; 85 | margin: 20px auto; 86 | font-weight: normal; 87 | } 88 | 89 | #main-progress { 90 | position: absolute; 91 | top: var(--title-size); 92 | } 93 | 94 | #main-progress:before { 95 | z-index: 1; 96 | } 97 | 98 | #content { 99 | position: absolute; 100 | width: 100%; 101 | top: var(--title-size); 102 | bottom: 0px; 103 | } 104 | 105 | .toast { 106 | background: #333; 107 | color: #FFF; 108 | padding: 10px; 109 | display: block; 110 | position: absolute; 111 | top: 80px; 112 | right: 10px; 113 | opacity: 0; 114 | } 115 | 116 | #hierarchy-picker i { 117 | font-size: 18px; 118 | } 119 | #hierarchy-picker.drag_over { 120 | box-shadow: 0 0 0 5px var(--button-bg-color); 121 | border: 5px dashed; 122 | box-sizing: border-box; 123 | line-height: 40px; 124 | } 125 | -------------------------------------------------------------------------------- /js/ddmlib/viewnode.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | const INT_MIN_VALUE = -2147483648; 17 | 18 | function VN_Property(fullname) { 19 | this.name = fullname; 20 | this.value = ""; 21 | this.type = "Uncategorized"; 22 | this.fullname = fullname; 23 | 24 | const colonIndex = fullname.indexOf(':'); 25 | if (colonIndex > 0) { 26 | const type = fullname.substring(0, colonIndex); 27 | this.type = type.charAt(0).toUpperCase() + type.slice(1); 28 | this.name = fullname.substring(colonIndex + 1); 29 | } 30 | } 31 | 32 | function ViewNode(name) { 33 | this.name = name; 34 | 35 | // List of VN_Property 36 | this.properties = []; 37 | 38 | // Map of name: VN_Property 39 | this.namedProperties = {}; 40 | 41 | // List of ViewNode 42 | this.children = []; 43 | } 44 | 45 | ViewNode.prototype.getBoolean = function(name, dValue) { 46 | const p = this.getProp(name); 47 | if (p) { 48 | return p.value == 'true'; 49 | } 50 | return dValue; 51 | } 52 | 53 | 54 | ViewNode.prototype.getInt = function(name, dValue) { 55 | const p = this.getProp(name); 56 | if (p) { 57 | try { 58 | return parseInt(p.value); 59 | } catch(e) {} 60 | } 61 | return dValue; 62 | } 63 | 64 | ViewNode.prototype.getFloat = function(name, dValue) { 65 | const p = this.getProp(name); 66 | if (p) { 67 | try { 68 | return parseFloat(p.value); 69 | } catch(e) {} 70 | } 71 | return dValue; 72 | } 73 | 74 | ViewNode.prototype.sortProperties = function() { 75 | this.properties.sort(function (a, b) { 76 | if (a.type > b.type) { 77 | return 1; 78 | } else if (a.type < b.type) { 79 | return -1 80 | } else if (a.name > b.name) { 81 | return 1; 82 | } else if (a.name < b.name) { 83 | return -1; 84 | } else { 85 | return 0; 86 | } 87 | }); 88 | } 89 | 90 | ViewNode.prototype.loadCommonProperties = function(map) { 91 | this.getProp = function(name) { 92 | return map ? this.namedProperties[map[name]] : this.namedProperties[name]; 93 | } 94 | 95 | this.id = this.getProp("id").value; 96 | this.left = this.getInt("left", 0); 97 | this.top = this.getInt("top", 0); 98 | this.width = this.getInt("width", 0); 99 | this.height = this.getInt("height", 0); 100 | this.scrollX = this.getInt("scrollX", 0); 101 | this.scrollY = this.getInt("scrollY", 0); 102 | this.willNotDraw = this.getBoolean("willNotDraw", false); 103 | 104 | this.clipChildren = this.getBoolean("clipChildren", true); 105 | this.translationX = this.getFloat("translationX", 0); 106 | this.translationY = this.getFloat("translationY", 0); 107 | this.scaleX = this.getFloat("scaleX", 1); 108 | this.scaleY = this.getFloat("scaleY", 1); 109 | 110 | let descProp = this.getProp("contentDescription"); 111 | this.contentDesc = descProp != null && descProp.value && descProp.value != "null" 112 | ? descProp.value : null; 113 | 114 | if (this.contentDesc == null) { 115 | descProp = this.getProp("text"); 116 | this.contentDesc = descProp != null && descProp.value && descProp.value != "null" 117 | ? descProp.value : null; 118 | } 119 | 120 | this.visibility = this.getProp("visibility").value; 121 | this.isVisible = !this.visibility || this.visibility.value == 0 || this.visibility.value == "VISIBLE"; 122 | 123 | delete this.getProp; 124 | } 125 | -------------------------------------------------------------------------------- /js/diff.js: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | class Diff { 16 | constructor() { 17 | this.withNewChildren = new Map() 18 | this.withRemovedChildren = new Map() 19 | this.withMovedBoxPos = [] 20 | this.withReorderedChildren = [] 21 | } 22 | 23 | markNewChild(parent, child) { 24 | let value = this.withNewChildren.get(parent) 25 | if (value == null) { 26 | value = [] 27 | this.withNewChildren.set(parent, value) 28 | } 29 | value.push(child) 30 | } 31 | 32 | markRemovedChild(parent, child) { 33 | let value = this.withRemovedChildren.get(parent) 34 | if (value == null) { 35 | value = [] 36 | this.withRemovedChildren.set(parent, value) 37 | } 38 | value.push(child) 39 | } 40 | } 41 | 42 | function compareNodes(newRootNode, oldRootNode) { 43 | const diff = new Diff() 44 | 45 | function inner(newNode, oldNode) { 46 | newNode.el = oldNode.el 47 | oldNode.el = null 48 | newNode.box = oldNode.box 49 | oldNode.box = null 50 | newNode.el.node = newNode 51 | newNode.box.node = newNode 52 | 53 | const finalOrderedChildren = [] 54 | const newChildrenMap = new Map(newNode.children.map((it) => [it.treeDisplayName, it] )) 55 | 56 | for (let i = 0; i < oldNode.children.length; i++) { 57 | const child = oldNode.children[i] 58 | const match = newChildrenMap.get(child.treeDisplayName) 59 | 60 | if (match == null) { 61 | diff.markRemovedChild(newNode, child) 62 | } else { 63 | inner(match, child) 64 | newChildrenMap.delete(child.treeDisplayName) 65 | finalOrderedChildren.push(child) 66 | } 67 | } 68 | for (let child of newChildrenMap.values()) { 69 | diff.markNewChild(newNode, child) 70 | finalOrderedChildren.push(child) 71 | } 72 | for (let i = 0; i < finalOrderedChildren.length; i++) { 73 | if (finalOrderedChildren[i].treeDisplayName != newNode.children[i].treeDisplayName) { 74 | diff.withReorderedChildren.push(newNode) 75 | break; 76 | } 77 | } 78 | if (hasDifferentBoxPosition(newNode, oldNode)) { 79 | diff.withMovedBoxPos.push(newNode) 80 | } 81 | } 82 | 83 | inner(newRootNode, oldRootNode) 84 | return diff 85 | } 86 | 87 | function hasDifferentBoxPosition(viewNode, other) { 88 | return viewNode.boxStylePos.top != other.boxStylePos.top 89 | || viewNode.boxStylePos.left != other.boxStylePos.left 90 | || viewNode.boxStylePos.width != other.boxStylePos.width 91 | || viewNode.boxStylePos.height != other.boxStylePos.height 92 | } 93 | 94 | function findDescendantById(viewNode, targetId) { 95 | if (viewNode.treeDisplayName == targetId) { 96 | return viewNode 97 | } 98 | for (let i = 0; i < viewNode.children.length; i++) { 99 | const maybeFound = findDescendantById(viewNode.children[i], targetId) 100 | if (maybeFound != null) { 101 | return maybeFound 102 | } 103 | } 104 | return null 105 | } 106 | 107 | /* returns a list of different properties and the different values */ 108 | function compareProperties(viewNode, other) { 109 | const PROPERTY_LIST = [ 110 | "classnameIndex", 111 | "id", 112 | "left", 113 | "top", 114 | "width", 115 | "height", 116 | "scrollX", 117 | "scrollY", 118 | "translationX", 119 | "translationY", 120 | "scaleX", 121 | "scaleY", 122 | "alpha", 123 | "willNotDraw", 124 | "clipChildren", 125 | "visibility", 126 | "elevation" 127 | ] 128 | 129 | const props = [] 130 | for (let pName of PROPERTY_LIST) { 131 | if (viewNode[pName] != other[pName]) { 132 | props.push({ name: pName, value: viewNode[pName], previousValue: other[pName] }) 133 | } 134 | } 135 | return props 136 | } -------------------------------------------------------------------------------- /js/ddmlib/jdwp.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | function jdwp(pid, device) { 16 | this.device = device; 17 | this.pid = pid; 18 | this.seq = 1; 19 | 20 | this.callbacks = []; 21 | 22 | this.status = this.STATUS_DISCONNECTED; 23 | this.pendingCalls = []; 24 | } 25 | 26 | jdwp.prototype.STATUS_DISCONNECTED = 0; 27 | jdwp.prototype.STATUS_CONNECTING = 1; 28 | jdwp.prototype.STATUS_CONNECTED = 2; 29 | 30 | jdwp.prototype._onDisconnect = function () { 31 | this.status = this.STATUS_DISCONNECTED; 32 | for (let i = 0; i < this.callbacks.length; i++) { 33 | if (this.callbacks[i]) { 34 | this.callbacks[i].reject(); 35 | } 36 | } 37 | this.callbacks = []; 38 | this.pendingCalls = []; 39 | this.socket = null; 40 | if (this.onClose) { 41 | this.onClose(); 42 | } 43 | } 44 | 45 | jdwp.prototype._connect = async function () { 46 | const that = this; 47 | this.status = this.STATUS_CONNECTING; 48 | 49 | const socket = this.device.openStream("jdwp:" + this.pid); 50 | socket.onClose = this._onDisconnect.bind(this); 51 | socket.keepOpen = true; 52 | 53 | const cmd = "JDWP-Handshake"; 54 | socket.write(cmd); 55 | this.socket = socket; 56 | 57 | var data = ab2str(await socket.read(cmd.length)) 58 | if (data != cmd) { 59 | socket.close(); 60 | return; 61 | } 62 | 63 | // Connected 64 | this.status = this.STATUS_CONNECTED; 65 | const calls = this.pendingCalls; 66 | this.pendingCalls = []; 67 | 68 | for (let i = 0; i < calls.length; i++) { 69 | this.socket.write(calls[i]); 70 | } 71 | 72 | // Do read loop 73 | while(true) { 74 | var data = await this.socket.read(11); 75 | const header = new DataInputStream(new Uint8Array(data)); 76 | const len = header.readInt(); 77 | const seq = header.readInt(); 78 | const flags = header.read(); 79 | const isCommand = flags != 128; 80 | 81 | data = await this.socket.read(len - 11); 82 | const reader = new DataInputStream(new Uint8Array(data)); 83 | const type = reader.readInt(); // chunk type 84 | reader.readInt(); // result length; 85 | 86 | if (isCommand) { 87 | console.log("Command received", type, getChunkType("APNM")); 88 | } else { 89 | reader.chunkType = type; 90 | this.callbacks[seq].accept(reader); 91 | this.callbacks[seq] = null; 92 | } 93 | } 94 | } 95 | 96 | jdwp.prototype.destroy = function () { 97 | this.pendingCalls = []; 98 | if (this.socket) { 99 | this.socket.close(); 100 | } 101 | } 102 | 103 | /** 104 | * @param type String or int chunk type 105 | * @param data byte array or DataOutputStream 106 | * @returns a promise for the result 107 | */ 108 | jdwp.prototype.writeChunk = function (type, data) { 109 | const result = deferred(); 110 | if (data.constructor == DataOutputStream) { 111 | data = data.data; 112 | } 113 | 114 | let packet = new DataOutputStream(); 115 | packet.writeInt(11 + 8 + data.length); // package length 116 | 117 | const seq = this.seq++; 118 | packet.writeInt(seq); 119 | 120 | packet.writeByte(0); // flags 121 | packet.writeByte(0xc7); // 'G' + 128 122 | packet.writeByte(0x01); // DDMS command 123 | 124 | packet.writeInt(getChunkType(type)); 125 | packet.writeInt(data.length); 126 | packet.writeBytes(data); 127 | packet = new Uint8Array(packet.data); 128 | 129 | this.callbacks[seq] = result; 130 | 131 | if (this.status != this.STATUS_CONNECTED) { 132 | this.pendingCalls.push(packet); 133 | if (this.status == this.STATUS_DISCONNECTED) { 134 | this._connect(); 135 | } 136 | } else { 137 | this.socket.write(packet); 138 | } 139 | return result; 140 | } 141 | 142 | const CHUNK_TYPES = {}; 143 | const getChunkType = function (type) { 144 | if (type.constructor == String) { 145 | if (CHUNK_TYPES[type] == undefined) { 146 | const buf = new ArrayBuffer(4); 147 | const arr = new Uint8Array(buf); 148 | for (let i = 0; i < 4; i++) { 149 | arr[3 - i] = type.charCodeAt(i); 150 | } 151 | CHUNK_TYPES[type] = new Int32Array(buf)[0]; 152 | } 153 | return CHUNK_TYPES[type]; 154 | } else { 155 | return type; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /third_party/auto-complete.min.js: -------------------------------------------------------------------------------- 1 | // JavaScript autoComplete v1.0.4 2 | // https://github.com/Pixabay/JavaScript-autoComplete 3 | var autoComplete=function(){function e(e){function t(e,t){return e.classList?e.classList.contains(t):new RegExp("\\b"+t+"\\b").test(e.className)}function o(e,t,o){e.attachEvent?e.attachEvent("on"+t,o):e.addEventListener(t,o)}function s(e,t,o){e.detachEvent?e.detachEvent("on"+t,o):e.removeEventListener(t,o)}function n(e,s,n,l){o(l||document,s,function(o){for(var s,l=o.target||o.srcElement;l&&!(s=t(l,e));)l=l.parentElement;s&&n.call(l,o)})}if(document.querySelector){var l={selector:0,source:0,minChars:3,delay:150,offsetLeft:0,offsetTop:1,cache:1,menuClass:"",renderItem:function(e,t){t=t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&");var o=new RegExp("("+t.split(" ").join("|")+")","gi");return'
'+e.replace(o,"$1")+"
"},onSelect:function(){}};for(var c in e)e.hasOwnProperty(c)&&(l[c]=e[c]);for(var a="object"==typeof l.selector?[l.selector]:document.querySelectorAll(l.selector),u=0;u0?i.sc.scrollTop=n+i.sc.suggestionHeight+s-i.sc.maxHeight:0>n&&(i.sc.scrollTop=n+s)}else i.sc.scrollTop=0},o(window,"resize",i.updateSC),document.body.appendChild(i.sc),n("autocomplete-suggestion","mouseleave",function(){var e=i.sc.querySelector(".autocomplete-suggestion.selected");e&&setTimeout(function(){e.className=e.className.replace("selected","")},20)},i.sc),n("autocomplete-suggestion","mouseover",function(){var e=i.sc.querySelector(".autocomplete-suggestion.selected");e&&(e.className=e.className.replace("selected","")),this.className+=" selected"},i.sc),n("autocomplete-suggestion","mousedown",function(e){if(t(this,"autocomplete-suggestion")){var o=this.getAttribute("data-val");i.value=o,l.onSelect(e,o,this),i.sc.style.display="none"}},i.sc),i.blurHandler=function(){try{var e=document.querySelector(".autocomplete-suggestions:hover")}catch(t){var e=0}e?i!==document.activeElement&&setTimeout(function(){i.focus()},20):(i.last_val=i.value,i.sc.style.display="none",setTimeout(function(){i.sc.style.display="none"},350))},o(i,"blur",i.blurHandler);var r=function(e){var t=i.value;if(i.cache[t]=e,e.length&&t.length>=l.minChars){for(var o="",s=0;st||t>40)&&13!=t&&27!=t){var o=i.value;if(o.length>=l.minChars){if(o!=i.last_val){if(i.last_val=o,clearTimeout(i.timer),l.cache){if(o in i.cache)return void r(i.cache[o]);for(var s=1;s= 0 && windowTopIndex >= 0) { 147 | root.windowX = globalProps[windowLeftIndex]; 148 | root.windowY = globalProps[windowTopIndex]; 149 | } 150 | return root; 151 | } 152 | -------------------------------------------------------------------------------- /js/adb/adb_common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Merger to read all data as text 3 | */ 4 | class TextResponseMerger { 5 | result = ""; 6 | #decoder = new TextDecoder(); 7 | 8 | merge(data) { 9 | this.result += this.#decoder.decode(data); 10 | } 11 | 12 | isComplete() { 13 | return false; 14 | } 15 | } 16 | 17 | class BaseAdbDevice { 18 | 19 | openStream(command) { 20 | // TODO Implement 21 | } 22 | 23 | closeAll() { 24 | // TODO Implement 25 | } 26 | 27 | async connect() { 28 | return true; 29 | } 30 | 31 | disconnect() { 32 | closeAll() 33 | } 34 | 35 | shellCommand(command) { 36 | return this.openStream("shell:" + command).readAll(); 37 | } 38 | 39 | 40 | async sendFile(targetPath, sourcePath) { 41 | let data = await doXhr(sourcePath, "arraybuffer"); 42 | const stream = this.openStream("sync:"); 43 | 44 | // Send request 45 | let out = new DataOutputStream(); 46 | out.highFirst = false; 47 | const path = new Uint8Array(stringToByteArray(targetPath + ",0755")); 48 | out.writeBytes(new Uint8Array(stringToByteArray("SEND"))); 49 | out.writeInt(path.length); 50 | out.writeBytes(path); 51 | stream.write(new Uint8Array(out.data)); 52 | 53 | // File data 54 | // TODO: Handle large files in 64k chunks 55 | data = new Uint8Array(data); 56 | out = new DataOutputStream(); 57 | out.highFirst = false; 58 | out.writeBytes(new Uint8Array(stringToByteArray("DATA"))); 59 | out.writeInt(data.length); 60 | out.writeBytes(data); 61 | stream.write(new Uint8Array(out.data)); 62 | 63 | // End of Data 64 | out = new DataOutputStream(); 65 | out.highFirst = false; 66 | out.writeBytes(new Uint8Array(stringToByteArray("DONE"))); 67 | out.writeInt(0); 68 | stream.write(new Uint8Array(out.data)); 69 | 70 | 71 | const response = await stream.read(4); 72 | const okay = ab2str(response); 73 | stream.close(); 74 | if ("OKAY" != okay) { 75 | throw "Transfer failer"; 76 | } 77 | } 78 | } 79 | 80 | class BaseAdbStream { 81 | 82 | onClose = null; 83 | keepOpen = false; 84 | 85 | #pending = []; 86 | #pendingRead = null; 87 | 88 | pendingCallback = null; 89 | pendingLength = 0; 90 | 91 | async write(data) { 92 | // TODO implement 93 | // return deferred 94 | } 95 | 96 | close() { 97 | // TODO implement 98 | } 99 | 100 | sendReady() { 101 | // TODO implement 102 | } 103 | 104 | onReceiveClose() { 105 | if (this.onClose) { 106 | this.onClose(); 107 | } 108 | } 109 | 110 | onReceiveWrite(data) { 111 | if (this.keepOpen) { 112 | this.sendReady(); 113 | } 114 | if (data && data.length) { 115 | this.#pending.push(data); 116 | } 117 | this.#doRead(); 118 | } 119 | 120 | read(length) { 121 | if (this.#pendingRead) { 122 | throw new Error("double callback"); 123 | } 124 | 125 | var result = deferred(length) 126 | this.#pendingRead = result; 127 | this.#doRead(); 128 | return result; 129 | } 130 | 131 | #doRead() { 132 | if (!this.#pendingRead) { 133 | return; 134 | } 135 | 136 | var length = this.#pendingRead.data; 137 | let result = null; 138 | let totalRead = 0; 139 | while (this.#pending.length) { 140 | let entry = this.#pending.shift(); 141 | if (!length) { 142 | result = entry; 143 | break 144 | } 145 | const remaining = length - totalRead; 146 | if (entry.byteLength > remaining) { 147 | // Add back extra bytes 148 | const tmp = entry.subarray(0, remaining); 149 | const extra = entry.subarray(remaining); 150 | this.#pending.unshift(extra); 151 | entry = tmp; 152 | } 153 | totalRead += entry.byteLength; 154 | result = result ? appendBuffer(result, entry) : entry; 155 | if (totalRead == length) break; 156 | } 157 | if (result != null && length != 0 && result.byteLength != length && result.byteLength != 0) { 158 | this.#pending.unshift(result); 159 | result = null; 160 | } 161 | if (result) { 162 | var callback = this.#pendingRead; 163 | this.#pendingRead = null; 164 | callback.accept(result); 165 | } 166 | } 167 | 168 | readAll(responseMerger) { 169 | const result = deferred(); 170 | if (!responseMerger) { 171 | responseMerger = new TextResponseMerger(); 172 | } 173 | 174 | this.onReceiveWrite = function (data) { 175 | this.sendReady(); 176 | responseMerger.merge(data); 177 | 178 | if (responseMerger.isComplete()) { 179 | this.close(); 180 | } 181 | }.bind(this); 182 | 183 | this.#pending.forEach(this.onReceiveWrite); 184 | this.onClose = function () { 185 | result.accept(responseMerger.result); 186 | } 187 | return result; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /js/ddmlib/property_formatter.js: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const EXCLUSION_LIST = new Set([ 16 | "toJSON", 17 | "$type", 18 | "constructor", 19 | "namedProperties", 20 | "properties", 21 | "children", 22 | "parent", 23 | "classname", 24 | "classnameIndex", 25 | "treeDisplayName", 26 | "hashcode", 27 | "name", 28 | "boxPos", 29 | "boxStylePos", 30 | "desc", 31 | "isVisible", 32 | "nodeDrawn" 33 | ]) 34 | 35 | const LAYOUT_TYPE = "Layout" 36 | const DRAWING_TYPE = "Drawing" 37 | const SCROLLING_TYPE = "Scrolling" 38 | const MISC_TYPE = "Misc" 39 | const DEFAULT_TYPE = "Unspecified" 40 | 41 | const PROPERTY_TYPE_MAP = new Map() 42 | PROPERTY_TYPE_MAP.set("left", LAYOUT_TYPE) 43 | PROPERTY_TYPE_MAP.set("top", LAYOUT_TYPE) 44 | PROPERTY_TYPE_MAP.set("width", LAYOUT_TYPE) 45 | PROPERTY_TYPE_MAP.set("height", LAYOUT_TYPE) 46 | PROPERTY_TYPE_MAP.set("elevation", LAYOUT_TYPE) 47 | PROPERTY_TYPE_MAP.set("scrollX", SCROLLING_TYPE) 48 | PROPERTY_TYPE_MAP.set("scrollY", SCROLLING_TYPE) 49 | PROPERTY_TYPE_MAP.set("translationX", DRAWING_TYPE) 50 | PROPERTY_TYPE_MAP.set("translationY", DRAWING_TYPE) 51 | PROPERTY_TYPE_MAP.set("scaleX", DRAWING_TYPE) 52 | PROPERTY_TYPE_MAP.set("scaleY", DRAWING_TYPE) 53 | PROPERTY_TYPE_MAP.set("alpha", DRAWING_TYPE) 54 | PROPERTY_TYPE_MAP.set("willNotDraw", DRAWING_TYPE) 55 | PROPERTY_TYPE_MAP.set("clipChildren", DRAWING_TYPE) 56 | PROPERTY_TYPE_MAP.set("visibility", MISC_TYPE) 57 | PROPERTY_TYPE_MAP.set("id", DEFAULT_TYPE) 58 | 59 | /* returns the root ViewNode that was passed in and altered */ 60 | const formatProperties = function(root /* ViewNode */, classNames /* string[] */) { 61 | function inner(node /* ViewNode */, 62 | maxW /* Int */, 63 | maxH /* Int */, 64 | leftShift /* Int */, 65 | topShift /* Int */, 66 | scaleX /* Int */, 67 | scaleY /* Int */) { 68 | const newScaleX = scaleX * node.scaleX 69 | const newScaleY = scaleY * node.scaleY 70 | 71 | const l = leftShift + (node.left + node.translationX) * scaleX + node.width * (scaleX - newScaleX) / 2 72 | const t = topShift + (node.top + node.translationY) * scaleY + node.height * (scaleY - newScaleY) / 2 73 | node.boxPos = { 74 | left: l, 75 | top: t, 76 | width: node.width * newScaleX, 77 | height: node.height * newScaleY, 78 | } 79 | 80 | node.boxStylePos = { 81 | left: (node.boxPos.left * 100 / maxW) + "%", 82 | top: (node.boxPos.top * 100 / maxH) + "%", 83 | width: (node.boxPos.width * 100 / maxW) + "%", 84 | height: (node.boxPos.height * 100 / maxH) + "%" 85 | } 86 | 87 | if (node.name == undefined) { 88 | node.name = node.classname 89 | } 90 | if (node.name == undefined && classNames) { 91 | node.name = classNames[node.classnameIndex] + "@" + node.hashcode; 92 | } 93 | 94 | node.treeDisplayName = node.name.split(".") 95 | node.treeDisplayName = node.treeDisplayName[node.treeDisplayName.length - 1]; 96 | 97 | if (node.contentDesc != null) { 98 | node.treeDisplayName = node.treeDisplayName + " : " + node.contentDesc; 99 | } 100 | node.desc = node.treeDisplayName; 101 | node.isVisible = node.visibility == 0 || node.visibility == "VISIBLE" || node.visibility == undefined 102 | node.nodeDrawn = !node.willNotDraw; 103 | 104 | for (let i = 0; i < node.children.length; i++) { 105 | inner(node.children[i], maxW, maxH, l - node.scrollX, t - node.scrollY, newScaleX, newScaleY); 106 | node.children[i].parent = node; 107 | node.nodeDrawn |= (node.children[i].nodeDrawn && node.children[i].isVisible); 108 | } 109 | 110 | if (node.properties == undefined) { 111 | node.properties = [] 112 | 113 | for (const propertyName in node) { 114 | if (!EXCLUSION_LIST.has(propertyName)) { 115 | const property = new VN_Property(propertyName) 116 | property.value = node[propertyName] 117 | property.type = PROPERTY_TYPE_MAP.get(propertyName) || DEFAULT_TYPE 118 | node.properties.push(property) 119 | } 120 | } 121 | 122 | node.properties.sort((a, b) => PROPERTY_TYPE_MAP.get(a.name)?.localeCompare(PROPERTY_TYPE_MAP.get(b.name))) 123 | } 124 | node.namedProperties = {}; 125 | for (let i = 0; i < node.properties.length; i++) { 126 | node.namedProperties[node.properties[i].fullname] = node.properties[i]; 127 | } 128 | } 129 | 130 | root.scaleX = root.scaleY = 1; 131 | root.translationX = root.translationY = 0; 132 | inner(root, root.width, root.height, 0, 0, 1, 1) 133 | return root 134 | } -------------------------------------------------------------------------------- /js/adb/adb_msg.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | const ADB_MESSAGE_HEADER_LENGTH = 24; 16 | 17 | const SYNC_COMMAND = 0x434e5953; 18 | const CNXN_COMMAND = 0x4e584e43; 19 | const OPEN_COMMAND = 0x4e45504f; 20 | const OKAY_COMMAND = 0x59414b4f; 21 | const CLSE_COMMAND = 0x45534c43; 22 | const WRTE_COMMAND = 0x45545257; 23 | const AUTH_COMMAND = 0x48545541; 24 | 25 | const AUTH_TYPE_TOKEN = 1; 26 | const AUTH_TYPE_SIGNATURE = 2; 27 | const AUTH_TYPE_RSAPUBLICKEY = 3; 28 | 29 | const commandMap = []; 30 | commandMap[SYNC_COMMAND] = "SYNC_COMMAND"; 31 | commandMap[CNXN_COMMAND] = "CNXN_COMMAND"; 32 | commandMap[OPEN_COMMAND] = "OPEN_COMMAND"; 33 | commandMap[OKAY_COMMAND] = "OKAY_COMMAND"; 34 | commandMap[CLSE_COMMAND] = "CLSE_COMMAND"; 35 | commandMap[WRTE_COMMAND] = "WRTE_COMMAND"; 36 | commandMap[AUTH_COMMAND] = "AUTH_COMMAND"; 37 | 38 | /** 39 | * ADB protocol version. 40 | */ 41 | const VERSION_SKIP_CHECKSUM = 0x01000001; 42 | const VERSION = VERSION_SKIP_CHECKSUM; 43 | 44 | /** 45 | * Compute the expected header magic value 46 | * command: number 47 | */ 48 | function computeAdbMessageHeaderMagic(command) { 49 | // >>> 0 forces to unsigned int 50 | return (command ^ 0xffffffff) >>> 0; 51 | } 52 | 53 | /** 54 | * data: Uint8Array 55 | */ 56 | function computeAdbMessageDataCrc32(data) { 57 | return data.reduce((sum, byte) => sum + byte, 0); 58 | } 59 | 60 | 61 | /** 62 | * Construct a header from the given properties for the given data. 63 | * command: number 64 | * arg0: number 65 | * arg1: number 66 | * data?: Uint8Array 67 | * @return AdbMessageHeader object 68 | */ 69 | function constructAdbHeader(command, arg0, arg1, data, version) { 70 | let checksum; 71 | if (version >= VERSION_SKIP_CHECKSUM && command != AUTH_COMMAND && command != CNXN_COMMAND) { 72 | checksum = 0; 73 | } else if (data) { 74 | checksum = computeAdbMessageDataCrc32(data); 75 | } else { 76 | checksum = 0; 77 | } 78 | return { 79 | command: command, 80 | arg0: arg0, 81 | arg1: arg1, 82 | data_length: data ? data.byteLength : 0, 83 | data_crc32: checksum, 84 | magic: computeAdbMessageHeaderMagic(command), 85 | }; 86 | } 87 | 88 | /** 89 | * Serialize a message header into bytes that should be sent to the device. 90 | * header: AdbMessageHeader 91 | * @see #constructAdbHeader 92 | */ 93 | function serializeAdbMessageHeader(header) { 94 | const buffer = new ArrayBuffer(ADB_MESSAGE_HEADER_LENGTH); 95 | const dataView = new DataView(buffer); 96 | dataView.setUint32(0, header.command, true); 97 | dataView.setUint32(4, header.arg0, true); 98 | dataView.setUint32(8, header.arg1, true); 99 | dataView.setUint32(12, header.data_length, true); 100 | dataView.setUint32(16, header.data_crc32, true); 101 | dataView.setUint32(20, header.magic, true); 102 | return new Uint8Array(buffer); 103 | } 104 | 105 | /** 106 | * Convert an ascii string to a byte array. 107 | */ 108 | function stringToByteArray(str) { 109 | const data = new Uint8Array(str.length); 110 | for (let i = 0; i < str.length; ++i) { 111 | data[i] = str.charCodeAt(i); 112 | } 113 | return data; 114 | } 115 | 116 | /** 117 | * Parse a message header from the buffer. Will throw if the header is 118 | * malformed. 119 | * data: Uint8Array 120 | */ 121 | function parseAndVerifyAdbMessageHeader(data) { 122 | if (data.byteLength !== ADB_MESSAGE_HEADER_LENGTH) { 123 | throw new Error(`Incorrect header size, ${data.byteLength}`); 124 | } 125 | const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength); 126 | const header = { 127 | command: dataView.getUint32(0, true), 128 | arg0: dataView.getUint32(4, true), 129 | arg1: dataView.getUint32(8, true), 130 | data_length: dataView.getUint32(12, true), 131 | data_crc32: dataView.getUint32(16, true), 132 | magic: dataView.getUint32(20, true), 133 | }; 134 | if (header.magic !== computeAdbMessageHeaderMagic(header.command)) { 135 | throw new Error('Header magic value mismatch'); 136 | } 137 | return header; 138 | } 139 | 140 | /** 141 | * Verify that the supplied data matches the header crc. 142 | * header: AdbMessageHeader 143 | * data: Uint8Array 144 | */ 145 | function verifyAdbMessageData(header, data) { 146 | if (header.data_crc32 !== computeAdbMessageDataCrc32(data)) { 147 | throw new Error('Data crc32 does not match header ' + header.data_crc32); 148 | } 149 | } 150 | 151 | /** 152 | * Appends to array buffers 153 | */ 154 | function appendBuffer(first, last) { 155 | const result = new Uint8Array(first.byteLength + last.byteLength); 156 | result.set(first, 0); 157 | result.set(last, first.byteLength); 158 | return result; 159 | } 160 | 161 | 162 | /** 163 | * Converts array buffer to string 164 | */ 165 | function ab2str(buf) { 166 | return String.fromCharCode.apply(null, buf); 167 | } 168 | -------------------------------------------------------------------------------- /css/dlist.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 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 | #device-list-content { 18 | margin: auto; 19 | max-width: 800px; 20 | overflow: auto; 21 | padding: 10px; 22 | } 23 | 24 | #device-list-content .error { 25 | margin-bottom: 10px; 26 | } 27 | 28 | .activity-list { 29 | background: var(--card-bg-color); 30 | color: var(--text-color); 31 | box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.2); 32 | border-radius: 8px; 33 | } 34 | 35 | .activity-list > .entry { 36 | padding: 10px 30px; 37 | border-left: 4px solid transparent; 38 | box-shadow: 0 1px var(--divider-color); 39 | min-height: 36px; 40 | transition: background 200ms; 41 | position: relative; 42 | } 43 | 44 | .activity-list > .entry:hover { 45 | border-color: var(--progress-color); 46 | background: var(--hover-bg-color); 47 | } 48 | 49 | .activity-list > .entry:last-child { 50 | box-shadow: none; 51 | border-radius: 0 0 8px 8px; 52 | } 53 | 54 | .activity-list > .entry:first-child { 55 | border-top-left-radius: 8px; 56 | border-top-right-radius: 8px; 57 | } 58 | 59 | .activity-list .title { 60 | font-weight: bold; 61 | margin-bottom: 4px; 62 | line-height: 16px; 63 | white-space: nowrap; 64 | text-overflow: ellipsis; 65 | overflow: hidden; 66 | } 67 | 68 | .activity-list .subtext { 69 | color: var(--subtext-color); 70 | display: inline-block; 71 | padding-right: 15px; 72 | line-height: 16px; 73 | } 74 | 75 | .activity-list .icon { 76 | width: 36px; 77 | height: 36px; 78 | float: left; 79 | margin-right: 8px; 80 | background-image: url(app_icon.png); 81 | background-size: 100%; 82 | position: relative; 83 | } 84 | 85 | .activity-list .icon.time-lapse::after { 86 | position: absolute; 87 | right: 0; 88 | bottom: 0; 89 | width: 20px; 90 | height: 20px; 91 | background: var(--filter-color); 92 | border-radius: 10px; 93 | content: " "; 94 | background-image: url("data:image/svg+xml;utf8,"); 95 | background-repeat: no-repeat; 96 | background-size: 13px; 97 | background-position: center; 98 | box-shadow: 0 0 3px rgba(0, 0, 0, .5); 99 | 100 | 101 | /* background-image: url("app_icon_under_chart-multiple.png"); */ 102 | } 103 | 104 | .old-api { 105 | --slider-size: 24px; 106 | 107 | display: inline-block; 108 | position: relative; 109 | height: var(--slider-size); 110 | line-height: var(--slider-size); 111 | padding: 0 calc(var(--slider-size) + var(--slider-size) + 5px); 112 | margin-bottom: 10px; 113 | color: var(--text-color); 114 | } 115 | 116 | .old-api input { 117 | display:none; 118 | } 119 | 120 | .old-api .slider { 121 | position: absolute; 122 | cursor: pointer; 123 | top: 0; 124 | left: 0; 125 | width: calc(var(--slider-size) + var(--slider-size) - 4px); 126 | bottom: 0; 127 | background-color: var(--button-bg-color); 128 | -webkit-transition: .4s; 129 | transition: .4s; 130 | border-radius: var(--slider-size); 131 | box-shadow: 0 1px 1px 1px rgba(0, 0, 0, 0.2); 132 | } 133 | 134 | .old-api .slider:before { 135 | position: absolute; 136 | content: ""; 137 | height: calc(var(--slider-size) - 8px); 138 | width: calc(var(--slider-size) - 8px); 139 | left: 4px; 140 | bottom: 4px; 141 | background-color: white; 142 | -webkit-transition: .4s; 143 | transition: .4s; 144 | border-radius: 50%; 145 | box-shadow: 0 1px 1px 1px rgba(0, 0, 0, 0.2); 146 | } 147 | 148 | .old-api input:checked + .slider { 149 | background-color: var(--title-color); 150 | } 151 | 152 | .old-api input:focus + .slider { 153 | box-shadow: 0 0 1px var(--title-color); 154 | } 155 | 156 | .old-api input:checked + .slider:before { 157 | -webkit-transform: translateX(calc(var(--slider-size) - 4px)); 158 | -ms-transform: translateX(calc(var(--slider-size) - 4px)); 159 | transform: translateX(calc(var(--slider-size) - 4px)); 160 | } 161 | 162 | #proxy-copy-command, #proxy-handshake-key { 163 | border: 1px solid var(--divider-color); 164 | display: inline-block; 165 | padding: 5px 8px; 166 | border-radius: 2px; 167 | line-height: 20px; 168 | cursor: default; 169 | } 170 | 171 | #proxy-copy-command:hover { 172 | border-color: var(--text-color); 173 | } 174 | 175 | #proxy-copy-command::before { 176 | content: "\e905"; 177 | font-family: "Icon-Font"; 178 | line-height: 20px; 179 | display: block; 180 | float: right; 181 | padding-left: 10px; 182 | } 183 | 184 | #proxy-handshake-key { 185 | outline: none; 186 | background: none; 187 | color: var(--text-color); 188 | cursor:text; 189 | width: 350px; 190 | margin-top: 5px; 191 | } -------------------------------------------------------------------------------- /js/adb/crypto.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | let AdbKey = (function () { 16 | const ADB_WEB_CRYPTO_ALGORITHM = { 17 | name: 'RSASSA-PKCS1-v1_5', 18 | hash: { 19 | name: 'SHA-1' 20 | }, 21 | }; 22 | 23 | const WORD_SIZE = 4; 24 | const MODULUS_SIZE_BITS = 2048; 25 | const MODULUS_SIZE = MODULUS_SIZE_BITS / 8; 26 | const MODULUS_SIZE_WORDS = MODULUS_SIZE / WORD_SIZE; 27 | const PUBKEY_ENCODED_SIZE = 3 * WORD_SIZE + 2 * MODULUS_SIZE; 28 | 29 | const PUBLIC_EXPONENT = new Uint8Array([0x01, 0x00, 0x01]); 30 | const ADB_WEB_CRYPTO_EXPORTABLE = true; 31 | const ADB_WEB_CRYPTO_OPERATIONS = ['sign']; 32 | 33 | const SIGNING_ASN1_PREFIX = [ 34 | 0x00, 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A, 0x05, 35 | 0x00, 0x04, 0x14 36 | ]; 37 | 38 | const R32 = BigInteger.ONE.shiftLeft(32); // 1 << 32 39 | 40 | function bigIntToFixedByteArray(bn, size) { 41 | // big-endian byte array 42 | const bytes = bn.toByteArray(); 43 | 44 | // Pad zeros if array isn't big enough 45 | while (bytes.length < size) { 46 | bytes.unshift(0); 47 | } 48 | 49 | // Remove extra zeros if array is too big 50 | while (bytes.length > size) { 51 | if (bytes[0] !== 0) { 52 | throw new Error('BigInteger value exceeds available size'); 53 | } 54 | bytes.shift(); 55 | } 56 | 57 | return bytes; 58 | } 59 | 60 | function encodeAndroidPublicKeyBytes(key) { 61 | const n0inv = R32.subtract(key.n.modInverse(R32)).intValue(); 62 | const r = BigInteger.ONE.shiftLeft(1).pow(MODULUS_SIZE_BITS); 63 | const rr = r.multiply(r).mod(key.n); 64 | 65 | const buffer = new ArrayBuffer(PUBKEY_ENCODED_SIZE); 66 | let dv = new DataView(buffer); 67 | dv.setUint32(0, MODULUS_SIZE_WORDS, true); 68 | dv.setUint32(WORD_SIZE, n0inv, true); 69 | new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength / Uint8Array.BYTES_PER_ELEMENT).set(bigIntToFixedByteArray(key.n, MODULUS_SIZE).reverse(), 2 * WORD_SIZE); 70 | new Uint8Array(dv.buffer, dv.byteOffset, dv.byteLength / Uint8Array.BYTES_PER_ELEMENT).set(bigIntToFixedByteArray(rr, MODULUS_SIZE).reverse(), 2 * WORD_SIZE + MODULUS_SIZE); 71 | dv.setUint32(2 * WORD_SIZE + 2 * MODULUS_SIZE, key.e, true); 72 | return new Uint8Array(buffer); 73 | } 74 | 75 | function padLeft(value, width, char) { 76 | const str = value.toString(); 77 | return char.repeat(Math.max(0, width - str.length)) + str; 78 | } 79 | 80 | /** 81 | * Decode the web safe base64url string to a hex number (assuming the encoded 82 | * data was a big endian number). 83 | */ 84 | function decodeWebBase64ToHex(str) { 85 | const bytes = atob(str.replace(/-/g, '+').replace(/_/g, '/')); 86 | let hex = ''; 87 | for (let i = 0; i < bytes.length; ++i) { 88 | hex += padLeft(bytes.charCodeAt(i).toString(16), 2, '0'); 89 | } 90 | return hex; 91 | } 92 | 93 | /* 94 | * Generates a new key and stores it in local storate 95 | */ 96 | async function generateNewKeyPair() { 97 | const keypair = await Promise.resolve(crypto.subtle.generateKey({ 98 | ...ADB_WEB_CRYPTO_ALGORITHM, 99 | modulusLength: MODULUS_SIZE_BITS, 100 | publicExponent: PUBLIC_EXPONENT, 101 | }, 102 | ADB_WEB_CRYPTO_EXPORTABLE, ADB_WEB_CRYPTO_OPERATIONS)); 103 | const jwk = await Promise.resolve(crypto.subtle.exportKey('jwk', keypair.publicKey)); 104 | 105 | const jsbnKey = new RSAKey(); 106 | jsbnKey.setPublic(decodeWebBase64ToHex(jwk.n), decodeWebBase64ToHex(jwk.e)); 107 | 108 | const bytes = encodeAndroidPublicKeyBytes(jsbnKey); 109 | const userInfo = 'unknown@web-hv'; 110 | 111 | const fullKey = await Promise.resolve(crypto.subtle.exportKey("jwk", keypair.privateKey)); 112 | fullKey.publicKey = btoa(String.fromCharCode.apply(null, bytes)) + ' ' + userInfo; 113 | 114 | localStorage.cryptoKey = JSON.stringify(fullKey); 115 | return localStorage.cryptoKey; 116 | } 117 | 118 | function AdbKeyInternal() { 119 | window.dd = this; 120 | 121 | this.fullKey = localStorage.cryptoKey; 122 | this.keyPromise = this.fullKey ? Promise.resolve(this.fullKey) : generateNewKeyPair(); 123 | } 124 | 125 | AdbKeyInternal.prototype.sign = function (token) { 126 | const jwk = JSON.parse(this.fullKey); 127 | 128 | key = new RSAKey(); 129 | key.setPrivateEx( 130 | decodeWebBase64ToHex(jwk.n), decodeWebBase64ToHex(jwk.e), 131 | decodeWebBase64ToHex(jwk.d), decodeWebBase64ToHex(jwk.p), 132 | decodeWebBase64ToHex(jwk.q), decodeWebBase64ToHex(jwk.dp), 133 | decodeWebBase64ToHex(jwk.dq), decodeWebBase64ToHex(jwk.qi)); 134 | 135 | // Message Layout (size equals that of the key modulus): 136 | // 00 01 FF FF FF FF ... FF [ASN.1 PREFIX] [TOKEN] 137 | const message = new Uint8Array(MODULUS_SIZE); 138 | 139 | // Initially just fill the buffer with the padding 140 | message.fill(0xFF); 141 | 142 | // add prefix 143 | message[0] = 0x00; 144 | message[1] = 0x01; 145 | 146 | // add the ASN.1 prefix 147 | message.set( 148 | SIGNING_ASN1_PREFIX, 149 | message.length - SIGNING_ASN1_PREFIX.length - token.length); 150 | 151 | // then the actual token at the end 152 | message.set(token, message.length - token.length); 153 | 154 | const messageInteger = new BigInteger(Array.apply([], message)); 155 | const signature = key.doPrivate(messageInteger); 156 | return new Uint8Array(bigIntToFixedByteArray(signature, MODULUS_SIZE)); 157 | } 158 | 159 | AdbKeyInternal.prototype.publicKey = async function () { 160 | const json = await this.keyPromise; 161 | const fullKey = JSON.parse(json); 162 | return fullKey.publicKey; 163 | } 164 | 165 | return AdbKeyInternal; 166 | 167 | })(); -------------------------------------------------------------------------------- /css/components.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Google LLC 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 | 18 | /* Progress Bar */ 19 | .progress-line, .progress-line:before { 20 | height: 4px; 21 | width: 100%; 22 | margin: 0; 23 | } 24 | .progress-line { 25 | background-color: var(--button-bg-color); 26 | display: -webkit-flex; 27 | display: flex; 28 | } 29 | .progress-line:before { 30 | background-color: var(--progress-color); 31 | content: ''; 32 | -webkit-animation: running-progress 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; 33 | } 34 | @-webkit-keyframes running-progress { 35 | 0% { margin-left: 0px; margin-right: 100%; } 36 | 50% { margin-left: 25%; margin-right: 0%; } 37 | 100% { margin-left: 100%; margin-right: 0; } 38 | } 39 | 40 | 41 | /* Error */ 42 | .error { 43 | display: inline-block; 44 | padding: 10px; 45 | border: 1px solid var(--error-color); 46 | border-radius: 4px; 47 | color: var(--error-color); 48 | } 49 | 50 | /* Scroll bars */ 51 | 52 | ::-webkit-scrollbar { 53 | width: 8px; 54 | height: 8px; 55 | } 56 | ::-webkit-scrollbar-track { 57 | background-color: var(--button-bg-color); 58 | } 59 | ::-webkit-scrollbar-thumb { 60 | background-color: var(--subtext-color); 61 | border-radius: 0px; 62 | } 63 | ::-webkit-scrollbar-corner { 64 | background-color: var(--button-bg-color); 65 | } 66 | 67 | /** Content panel animation **/ 68 | .content-panel { 69 | position:absolute; 70 | left:0; 71 | top:0; 72 | right:0; 73 | bottom: 0; 74 | } 75 | 76 | #device-list-content.hide, .content-panel.hide { 77 | -webkit-animation:fade-out-and-hide 300ms; 78 | -webkit-animation-fill-mode: forwards; 79 | } 80 | 81 | @-webkit-keyframes fade-out-and-hide { 82 | 0% { 83 | transform: scale(1); 84 | opacity: 1; 85 | } 86 | 100% { 87 | transform: scale(0.98); 88 | opacity: 0; 89 | visibility: hidden; 90 | } 91 | } 92 | 93 | .hidden { 94 | display: none; 95 | } 96 | 97 | /** Tree view **/ 98 | .treeview label { 99 | display: block; 100 | clear: both; 101 | cursor: pointer; 102 | line-height: 27px; 103 | -webkit-user-select: none; 104 | } 105 | 106 | .treeview label x-line-wrap { 107 | display: inline-block; 108 | transition: all .2s; 109 | white-space: nowrap; 110 | padding: 0 15px; 111 | border-radius: 2px; 112 | } 113 | 114 | .treeview label:is(:hover, .selected:hover, .last_selected:hover, 115 | .hover, .selected.hover, .last_selected.hover) x-line-wrap { 116 | background: var(--under-cursor-color); 117 | } 118 | 119 | .treeview label.selected x-line-wrap { 120 | background: var(--selected-color); 121 | } 122 | 123 | .treeview label.last_selected x-line-wrap { 124 | background: var(--last-selected-color); 125 | } 126 | 127 | .treenode { 128 | padding-left: 15px; 129 | } 130 | 131 | .treeview label.with_arrow { 132 | padding: 0 25px 0 0; 133 | } 134 | 135 | label.with_arrow span:before { 136 | content: ' '; 137 | display: inline-block; 138 | width: 25px; 139 | } 140 | 141 | label.expandable.with_arrow span:before { 142 | float: left; 143 | font-family: "Icon-Font"; 144 | content: '\e5c5'; /* arrow_drop_down */ 145 | font-size: 18px; 146 | text-align: center; 147 | transition: all .1s; 148 | } 149 | 150 | label.expandable.closed.with_arrow span:before { 151 | transform: rotate(-90deg); 152 | } 153 | 154 | /** Search box **/ 155 | input[type=search]::-webkit-search-cancel-button { 156 | -webkit-appearance: none; 157 | --icon-size: 16px; 158 | height: var(--icon-size); 159 | width: var(--icon-size); 160 | display: block; 161 | background-image: var(--clear-icon); 162 | background-repeat: no-repeat; 163 | background-size: var(--icon-size); 164 | } 165 | input[type=search]::-webkit-search-cancel-button:hover { 166 | opacity: 0.8; 167 | } 168 | 169 | 170 | /** Context menu **/ 171 | .context-wrapper { 172 | position: absolute; 173 | left: 0; 174 | top: 0; 175 | width: 100%; 176 | height: 100%; 177 | background: transparent; 178 | z-index: 10000; 179 | } 180 | .contextmenu { 181 | position: absolute; 182 | background: var(--window-bg-color); 183 | box-shadow: 0 2px 4px rgba(0,0,0,0.2); 184 | font-size: 13px; 185 | left: 30px; 186 | top: 30px; 187 | } 188 | .contextmenu a, .contextmenu input { 189 | display: block; 190 | padding: 6px 15px; 191 | line-height: 27px; 192 | min-width: 200px; 193 | cursor: pointer; 194 | transition: all 100ms; 195 | -webkit-user-select: none; 196 | margin-left: 0px; 197 | } 198 | .contextmenu a::before { 199 | font-size: 150%; 200 | margin-right: 10px; 201 | float: left; 202 | } 203 | .contextmenu a:hover, .contextmenu a.selected { 204 | background: var(--hover-bg-color); 205 | } 206 | .contextmenu a:active { 207 | color: var(--selected-color); 208 | } 209 | .contextmenu a.separator { 210 | border-top: 1px solid var(--divider-color); 211 | } 212 | 213 | .contextmenu a.disabled { 214 | opacity: 0.5; 215 | background: none; 216 | } 217 | 218 | /** Simple button **/ 219 | button { 220 | background-color: var(--button-bg-color); 221 | border: none; 222 | border-radius: 4px; 223 | line-height: 30px; 224 | height: 30px; 225 | padding: 0 20px; 226 | box-shadow: 0 0 1px 2px rgba(0, 0, 0, 0.2); 227 | font-weight: bold; 228 | transition: color .1s , box-shadow .1s , transform .1s; 229 | outline: none; 230 | color: var(--text-color); 231 | } 232 | 233 | button.icon_btn { 234 | padding-left: 0px; 235 | } 236 | button.icon_btn::before { 237 | padding: 0 5px 0 10px; 238 | font-size: 140%; 239 | float: left; 240 | } 241 | 242 | button:focus { 243 | box-shadow: 0 0 0 1px var(--selected-color); 244 | } 245 | 246 | button:hover { 247 | box-shadow: inset 0 0 100px 100px var(--hover-bg-color), 0 0 1px 2px rgba(0, 0, 0, 0.2); 248 | } 249 | button:active { 250 | box-shadow: inset 0 0 100px 100px var(--hover-bg-color), 0 0 1px 2px rgba(0, 0, 0, 0.4); 251 | transform: scale(0.98); 252 | } 253 | 254 | 255 | .button-bar { 256 | display: flex; 257 | margin-bottom: 5px; 258 | } 259 | .button-group > button { 260 | box-shadow: none; 261 | border-radius: 0px; 262 | margin-right:1px 263 | } 264 | .button-group > button:first-child { 265 | border-radius: 4px 0px 0px 4px; 266 | } 267 | .button-group > button:last-child { 268 | border-radius: 0px 4px 4px 0px; 269 | } 270 | 271 | 272 | .contextmenu input[type=search] { 273 | padding: 2px 15px; 274 | -webkit-appearance: none; 275 | border-radius: 0px; 276 | border: 0; 277 | max-width: 500px; 278 | width: calc(100vw - 40px); 279 | cursor: default; 280 | } -------------------------------------------------------------------------------- /js/utils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | function deferred(data) { 16 | let a, r; 17 | const p = new Promise(function(accept, reject) { 18 | a = accept; 19 | r = reject; 20 | }); 21 | p.accept = a; 22 | p.reject = r; 23 | p.data = data; 24 | return p; 25 | } 26 | 27 | class Mutex { 28 | constructor() { 29 | this._lock = Promise.resolve(); 30 | } 31 | 32 | lock() { 33 | const nextLock = deferred(); 34 | const returnAfterCurrentLock = this._lock.then(() => nextLock.accept); 35 | this._lock = this._lock.then(() => nextLock); 36 | return returnAfterCurrentLock; 37 | } 38 | } 39 | 40 | // eslint-disable-next-line prefer-const 41 | let ActiveState = []; 42 | 43 | function createWorker(url) { 44 | const worker = new Worker(url); 45 | ActiveState.push(function() { 46 | worker.terminate(); 47 | }); 48 | return worker; 49 | } 50 | 51 | function createUrl(data) { 52 | const url = URL.createObjectURL(data); 53 | ActiveState.push(function() { 54 | URL.revokeObjectURL(url); 55 | }); 56 | return url; 57 | } 58 | 59 | function doXhr(url, responseType) { 60 | const result = deferred(); 61 | const xhr = new XMLHttpRequest(); 62 | xhr.onreadystatechange = function() { 63 | if (this.readyState == 4) { 64 | if (this.status == 200) { 65 | result.accept(this.response); 66 | } else { 67 | result.reject(); 68 | } 69 | } 70 | } 71 | xhr.open('GET', url); 72 | xhr.responseType = responseType; 73 | xhr.send() 74 | return result; 75 | } 76 | 77 | async function saveFile(fileName, url) { 78 | const a = $("").attr({href:url, download:fileName}).appendTo(document.body); 79 | a.get(0).click(); 80 | setTimeout(function() { 81 | a.remove(); 82 | }, 0); 83 | } 84 | 85 | function showContext(menu, callback, e) { 86 | const elementFactory = function(el, hideMenu) { 87 | const menuClickHandler = function() { 88 | if (!$(this).hasClass(CLS_DISABLED)) { 89 | if (!callback.call($(this).data("info"), $(this))) { 90 | hideMenu(); 91 | } 92 | } 93 | }; 94 | 95 | let addSeparator = false; 96 | for (let i = 0; i < menu.length; i++) { 97 | const m = menu[i]; 98 | if (!m) { 99 | addSeparator = true; 100 | continue; 101 | } 102 | const item = $("").text(m.text).addClass(m.icon).appendTo(el).data("info", m).click(menuClickHandler); 103 | if (addSeparator) { 104 | item.addClass("separator"); 105 | } 106 | if (m.disabled) { 107 | item.addClass(CLS_DISABLED); 108 | } 109 | addSeparator = false; 110 | } 111 | } 112 | 113 | showPopup(e, elementFactory); 114 | } 115 | 116 | function showInputPopup(e, value, placeHolder) { 117 | let input; 118 | let errorContainer; 119 | const popup = showPopup(e, function(el, hideMenu) { 120 | input = $(``) 121 | .appendTo(el) 122 | .keydown(function (e) { 123 | if (e.keyCode == 27) { 124 | e.preventDefault(); 125 | hideMenu(); 126 | } 127 | }); 128 | errorContainer = $("
").appendTo(el); 129 | }) 130 | popup.inputBox = input; 131 | 132 | input.val(value).focus().select(); 133 | input 134 | .keyup(function (e) { 135 | if (popup.ignoreNextKeyUp) { 136 | popup.ignoreNextKeyUp = false; 137 | return; 138 | } 139 | if (e.keyCode == 13) { 140 | popup.trigger("value_input", [$(this).val(), e.shiftKey]) 141 | } 142 | }) 143 | .on("input", () => errorContainer.empty()) 144 | .blur(() => errorContainer.empty()); 145 | popup.get(0).showError = e => errorContainer.showError(e); 146 | return popup; 147 | } 148 | 149 | /** 150 | * @param {*} e the click event 151 | * @param {*} elementFactory a function which tasks 2 arguments: , 152 | */ 153 | function showPopup(e, elementFactory) { 154 | if (e.preventDefault) { 155 | e.preventDefault(); 156 | } 157 | const wrapper = $("
").appendTo(document.body); 158 | const el = $("
").appendTo(wrapper); 159 | 160 | const documentMouseDown = function(e) { 161 | if (!el.has(e.toElement).length) { 162 | hideMenu(); 163 | } 164 | }; 165 | 166 | $(document).mousedown(documentMouseDown); 167 | const hideMenu = function() { 168 | wrapper.remove(); 169 | $(document).unbind("mousedown", documentMouseDown); 170 | wrapper.trigger("popup_closed"); 171 | } 172 | 173 | elementFactory(el, hideMenu); 174 | wrapper.get(0).hideMenu = hideMenu; 175 | el.show().css({ 176 | left: Math.min(e.pageX, $(document).width() - el.width() - 10), 177 | top: Math.min(e.pageY, $(document).height() - el.height() - 10)}); 178 | 179 | return wrapper; 180 | } 181 | 182 | function toast(msg) { 183 | $("
").text(msg).appendTo($("#content")).animate({top: 10, opacity:1}).delay(5000).fadeOut(300, function() { $(this).remove(); }); 184 | } 185 | 186 | /** 187 | * scrolls the parent such that the child is in view 188 | */ 189 | function scrollToView(child, parent) { 190 | // scroll To View 191 | const pTop = parent.stop().offset().top; 192 | const elTop = child.stop().offset().top; 193 | let delta = 0; 194 | if (elTop < pTop) { 195 | delta = elTop - pTop - 20; 196 | } else if ((elTop + child.height()) > pTop + parent.height()) { 197 | delta = elTop + child.height() - pTop - parent.height() + 20; 198 | } 199 | if (delta != 0) { 200 | parent.animate({scrollTop: parent.scrollTop() + delta}, 300); 201 | } 202 | } 203 | 204 | function base64ToUint8Array(base64String) { 205 | const binary_string = atob(base64String); 206 | const len = binary_string.length; 207 | const bytes = new Uint8Array( len ); 208 | for (let i = 0; i < len; i++) { 209 | const ascii = binary_string.charCodeAt(i); 210 | bytes[i] = ascii; 211 | } 212 | return bytes 213 | } 214 | 215 | class ExtendedDisplay { 216 | constructor(width, height, dpi) { 217 | this.width = width; 218 | this.height = height; 219 | this.dpi = dpi; 220 | } 221 | isSameAs(other) { 222 | return this.width == other.width && this.height == other.height && this.dpi == other.dpi; 223 | } 224 | toMenuItem() { 225 | return { 226 | text: `${this.width} x ${this.height} @ ${this.dpi} dpi`, 227 | display: this, 228 | id: 100 229 | } 230 | } 231 | toUrlParams() { 232 | return `?mode=secondary&width=${this.width}&height=${this.height}&dpi=${this.dpi}` 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /commands/src/main/java/ProcessIcon.java: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import static android.content.Context.ACTIVITY_SERVICE; 16 | import static android.graphics.Paint.ANTI_ALIAS_FLAG; 17 | import static android.graphics.Paint.DITHER_FLAG; 18 | import static android.graphics.Paint.FILTER_BITMAP_FLAG; 19 | 20 | import android.app.ActivityManager; 21 | import android.app.ActivityManager.RunningAppProcessInfo; 22 | import android.content.Context; 23 | import android.content.pm.PackageManager.NameNotFoundException; 24 | import android.graphics.Bitmap; 25 | import android.graphics.Bitmap.CompressFormat; 26 | import android.graphics.Bitmap.Config; 27 | import android.graphics.Canvas; 28 | import android.graphics.Color; 29 | import android.graphics.Paint; 30 | import android.graphics.PaintFlagsDrawFilter; 31 | import android.graphics.Rect; 32 | import android.graphics.drawable.AdaptiveIconDrawable; 33 | import android.graphics.drawable.Drawable; 34 | import android.graphics.drawable.DrawableWrapper; 35 | import android.os.Build.VERSION; 36 | import android.os.Build.VERSION_CODES; 37 | import android.os.Looper; 38 | import android.util.Base64; 39 | 40 | import java.io.ByteArrayOutputStream; 41 | 42 | /** 43 | * Simple program which fetches the app icon for a given process. 44 | */ 45 | public class ProcessIcon { 46 | 47 | // Percent of actual icon size 48 | private static final float ICON_SIZE_BLUR_FACTOR = 0.5f/48; 49 | // Percent of actual icon size 50 | private static final float ICON_SIZE_KEY_SHADOW_DELTA_FACTOR = 1f/48; 51 | 52 | private static final int KEY_SHADOW_ALPHA = 61; 53 | private static final int AMBIENT_SHADOW_ALPHA = 30; 54 | 55 | private static final int ICON_SIZE = 96; 56 | 57 | public static void main(String[] args) { 58 | Context context = getContext(); 59 | if (context == null) { 60 | System.out.println("FAIL"); 61 | return; 62 | } 63 | 64 | String packageName = args[0]; 65 | try { 66 | int pid = Integer.parseInt(args[0]); 67 | packageName = getPackageName(context, pid); 68 | } catch (NumberFormatException e) { 69 | // Ignore 70 | } 71 | if (packageName == null) { 72 | System.out.println("FAIL"); 73 | return; 74 | } 75 | 76 | Drawable icon; 77 | try { 78 | icon = context.getPackageManager().getApplicationIcon(packageName); 79 | } catch (NameNotFoundException e) { 80 | System.out.println("FAIL"); 81 | return; 82 | } 83 | icon = wrapIconDrawableWithShadow(icon); 84 | 85 | Bitmap bitmap = Bitmap.createBitmap(ICON_SIZE, ICON_SIZE, Config.ARGB_8888); 86 | Canvas canvas = new Canvas(bitmap); 87 | canvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, 88 | FILTER_BITMAP_FLAG | ANTI_ALIAS_FLAG)); 89 | canvas.scale(0.1f, 0.1f); 90 | 91 | icon.setBounds(0, 0, ICON_SIZE * 10, ICON_SIZE * 10); 92 | icon.draw(canvas); 93 | 94 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 95 | bitmap.compress(CompressFormat.PNG, 100, out); 96 | String iconText = Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP); 97 | 98 | // Everything initialized. Send OKAY. 99 | System.out.println("OKAY"); 100 | System.out.println(iconText); 101 | } 102 | 103 | private static String getPackageName(Context context, int pid) { 104 | ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); 105 | for (RunningAppProcessInfo info : am.getRunningAppProcesses()) { 106 | if (info.pid == pid) { 107 | if (info.pkgList.length > 0) { 108 | return info.pkgList[0]; 109 | } else { 110 | return null; 111 | } 112 | } 113 | } 114 | return null; 115 | } 116 | 117 | public static Context getContext() { 118 | try { 119 | Looper.prepare(); 120 | 121 | Class atc = Class.forName("android.app.ActivityThread"); 122 | Object systemThread = atc.getDeclaredMethod("systemMain").invoke(null); 123 | return (Context) atc.getDeclaredMethod("getSystemContext").invoke(systemThread); 124 | } catch (Exception e) { 125 | return null; 126 | } 127 | } 128 | 129 | public static Drawable wrapIconDrawableWithShadow(Drawable drawable) { 130 | if (VERSION.SDK_INT < VERSION_CODES.O || !(drawable instanceof AdaptiveIconDrawable)) { 131 | return drawable; 132 | } 133 | Bitmap shadow = getShadowBitmap((AdaptiveIconDrawable) drawable); 134 | return new ShadowDrawable(shadow, drawable); 135 | } 136 | 137 | private static Bitmap getShadowBitmap(AdaptiveIconDrawable d) { 138 | int shadowSize = Math.max(ICON_SIZE, d.getIntrinsicHeight()); 139 | d.setBounds(0, 0, shadowSize, shadowSize); 140 | 141 | float blur = ICON_SIZE_BLUR_FACTOR * shadowSize; 142 | float keyShadowDistance = ICON_SIZE_KEY_SHADOW_DELTA_FACTOR * shadowSize; 143 | 144 | int bitmapSize = (int) (shadowSize + 2 * blur + keyShadowDistance); 145 | Bitmap shadow = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888); 146 | 147 | Canvas canvas = new Canvas(shadow); 148 | canvas.translate(blur + keyShadowDistance / 2, blur); 149 | 150 | Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 151 | paint.setColor(Color.TRANSPARENT); 152 | 153 | // Draw ambient shadow 154 | paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24); 155 | canvas.drawPath(d.getIconMask(), paint); 156 | 157 | // Draw key shadow 158 | canvas.translate(0, keyShadowDistance); 159 | paint.setShadowLayer(blur, 0, 0, KEY_SHADOW_ALPHA << 24); 160 | canvas.drawPath(d.getIconMask(), paint); 161 | canvas.setBitmap(null); 162 | return shadow; 163 | } 164 | 165 | /** 166 | * A drawable which draws a shadow bitmap behind a drawable 167 | */ 168 | private static class ShadowDrawable extends DrawableWrapper { 169 | 170 | private final Bitmap mShadow; 171 | private final Paint mPaint = new Paint(FILTER_BITMAP_FLAG | ANTI_ALIAS_FLAG); 172 | 173 | public ShadowDrawable(Bitmap shadow, Drawable dr) { 174 | super(dr); 175 | mShadow = shadow; 176 | } 177 | 178 | @Override 179 | public void draw(Canvas canvas) { 180 | Rect bounds = getBounds(); 181 | canvas.drawBitmap(mShadow, null, bounds, mPaint); 182 | canvas.save(); 183 | // Ratio of child drawable size to shadow bitmap size 184 | float factor = 1 / (1 + 2 * ICON_SIZE_BLUR_FACTOR + ICON_SIZE_KEY_SHADOW_DELTA_FACTOR); 185 | 186 | canvas.translate( 187 | bounds.width() * factor * 188 | (ICON_SIZE_BLUR_FACTOR + ICON_SIZE_KEY_SHADOW_DELTA_FACTOR / 2), 189 | bounds.height() * factor * ICON_SIZE_BLUR_FACTOR); 190 | canvas.scale(factor, factor); 191 | super.draw(canvas); 192 | canvas.restore(); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /js/activity_list.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /* Action to refresh activity list */ 16 | var activityListAction = function (initializer, skipPush) { 17 | if (!skipPush) { 18 | backStack.add("?activity_list", () => activityListAction(initializer, true)); 19 | } 20 | progress.show(); 21 | var content = $("#device-list-content").empty().show(); 22 | $("#hview, #dmirrorview").addClass("hide").addClass("hidden"); 23 | 24 | let jdwpErrorContainer; 25 | let windowLoaded; 26 | const mainContent = $("
").appendTo(content); 27 | 28 | let newApiChk = null; 29 | 30 | const startHView = function () { 31 | const info = $(this).data("appInfo"); 32 | if (newApiChk != null && newApiChk.is(':checked')) { 33 | info.use_new_api = false; 34 | } 35 | 36 | if (info.isTimeLapse) { 37 | tlHvAction(info) 38 | } else { 39 | hViewAction(info); 40 | } 41 | } 42 | 43 | const showExtendDisplay = function() { 44 | const defaulValue = new ExtendedDisplay(2560, 1600, 320); 45 | let favorites = [] 46 | 47 | if (localStorage.favoriteDisplays) { 48 | try { 49 | const tmp = JSON.parse(localStorage.favoriteDisplays); 50 | if (tmp && tmp.constructor == Array) { 51 | for (var i = 0; i < tmp.length; i++) { 52 | if (tmp[i].width > 0 && tmp[i].height > 0 && tmp[i].dpi > 0) { 53 | let thisDisplay = new ExtendedDisplay(tmp[i].width, tmp[i].height, tmp[i].dpi); 54 | if (!defaulValue.isSameAs(thisDisplay) && favorites.length < 3 && !favorites.find(e => thisDisplay.isSameAs(e))) { 55 | favorites.push(thisDisplay); 56 | } 57 | } 58 | } 59 | } 60 | } catch(e) { } 61 | } 62 | 63 | const menu = [ 64 | { text: "Secondary Display", disabled: true }, 65 | defaulValue.toMenuItem(), 66 | ...favorites.map(e => e.toMenuItem()), 67 | null, 68 | { text: "Custom size", id: 1 } 69 | ]; 70 | const offset = $(this).offset(); 71 | const popupEvent = {pageX: offset.left, pageY: offset.top + $(this).height()}; 72 | 73 | showContext(menu, function (el) { 74 | if (this.id == 1) { 75 | showInputPopup(popupEvent, "1280 x 720 @ 240 dpi", " x @ dpi") 76 | .on("value_input", function(e, val) { 77 | let parsed = val.match(/^\s*(\d+)\s*x\s*(\d+)\s*\@\s*(\d+)\s*dpi\s*$/) 78 | if (!parsed) { 79 | this.showError("Invalid display description") 80 | return; 81 | } 82 | extendDisplay(new ExtendedDisplay(parseInt(parsed[1]), parseInt(parsed[2]), parseInt(parsed[3]))) 83 | this.hideMenu(); 84 | }) 85 | } else if (this.display) { 86 | extendDisplay(this.display) 87 | } 88 | }, 89 | popupEvent); 90 | 91 | const extendDisplay = function(thisDisplay) { 92 | if (!defaulValue.isSameAs(thisDisplay)) { 93 | favorites = favorites.filter(e => !defaulValue.isSameAs(e)) 94 | favorites.unshift(thisDisplay) 95 | localStorage.favoriteDisplays = JSON.stringify(favorites) 96 | } 97 | 98 | deviceMirrorAction(thisDisplay); 99 | } 100 | } 101 | 102 | const renderActivities = function(container, list) { 103 | const buttonbar = $("
").appendTo(container); 104 | if (list.use_new_api) { 105 | newApiChk = $(''); 106 | $("
").css({flexGrow: 1}).appendTo(buttonbar); 112 | $("
").addClass("button-group").appendTo(buttonbar) 113 | .append($(" 72 | or 73 | 74 | 75 | Selecting a device will kill existing ADB connections 76 |
77 | 78 |

Authorized devices

79 |
80 | 81 |

Adb Proxy devices

82 |
83 |

Get Android Web Device Proxy to enable sharing ADB connections with other development tools

84 |
85 | 86 |
87 | 88 |
89 | 90 | 91 | 157 | 158 | 159 | 163 | -------------------------------------------------------------------------------- /js/file_load_worker.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | importScripts("../third_party/jszip.min.js"); 16 | importScripts("constants.js"); 17 | importScripts("utils.js") 18 | 19 | self.onmessage = function(e) { 20 | const reader = new FileReader(); 21 | reader.onload = function () { 22 | handleLoadFile(reader).catch(function(e) { 23 | postMessage({type: TYPE_ERROR, message: e + ""}); 24 | }); 25 | } 26 | reader.readAsArrayBuffer(e.data); 27 | } 28 | 29 | async function handleLoadFile(reader) { 30 | const zip = new JSZip(reader.result); 31 | 32 | // Try loading as bug report 33 | { 34 | const list = []; 35 | const display_size = { }; 36 | 37 | // Check for visible_windows.zip 38 | const viewDump = zip.file("visible_windows.zip"); 39 | if (viewDump != null) { 40 | try { 41 | const viewDumpZip = new JSZip(viewDump.asArrayBuffer()); 42 | for (x in viewDumpZip.files) { 43 | list.push({ 44 | name: x, 45 | data: viewDumpZip.files[x].asUint8Array(), 46 | type: TYPE_BUG_REPORT_V2, 47 | display: display_size 48 | }) 49 | } 50 | } catch (e) { 51 | console.log("Error", e); 52 | } 53 | } 54 | 55 | loadTimeLapseFiles(zip, list) 56 | const bugFile = zip.file(/^bugreport/); 57 | if (bugFile != null && bugFile.length == 1) { 58 | return loadBugFile(bugFile[0], list); 59 | } else if (list.length > 0) { 60 | list.use_new_api = false; 61 | return postMessage({type: TYPE_BUG_REPORT, list: list}); 62 | } 63 | } 64 | 65 | const config = JSON.parse(zip.file("config.json").asText()); 66 | const appInfo = { type: TYPE_ZIP, data: reader.result, config: config, name: config.title }; 67 | 68 | if (config.version != 1 || !config.title || zip.file("hierarchy.txt") == null) { 69 | throw "Missing data" 70 | } 71 | 72 | postMessage(appInfo); 73 | } 74 | 75 | function loadTimeLapseFiles(zip, list) { 76 | zip.folder(WM_TRACE_DIR).file(VIEW_CAPTURE_REGEX).forEach(file => { 77 | list.push({ 78 | name: file.name.substring(file.name.lastIndexOf("/")+1, file.name.lastIndexOf(".")), 79 | data: file.asUint8Array(), 80 | type: TYPE_TIME_LAPSE_BUG_REPORT, 81 | isTimeLapse: true, 82 | display: { } 83 | }) 84 | }) 85 | } 86 | 87 | async function loadBugFile(bugFile, list) { 88 | if (list.length == 0) { 89 | throw "No hierarchy found"; 90 | } 91 | const liner = newLiner(bugFile.asArrayBuffer()); 92 | const PARSING_DATA = [ 93 | { 94 | key: "windows", 95 | header: "WINDOW MANAGER WINDOWS (dumpsys window windows)", 96 | titleRegX: /^(\s+)Window #\d+ Window\{([a-zA-Z\d]+) [a-zA-Z\d]+ ([^}\s]+)\}:/, 97 | titleGroups: { 98 | hashCode: 2, 99 | name: 3, 100 | }, 101 | 102 | entries: { 103 | ownerUid: / mOwnerUid=(\d+) /, 104 | display: / mFullConfiguration=\{[^}]*\b(\d+, \d+)/, 105 | dpi: / mFullConfiguration=\{[^}]*\b(\d+)dpi\b/ 106 | } 107 | }, 108 | { 109 | key: "packages", 110 | header: "Packages:", 111 | titleRegX: /^(\s+)Package \[([a-z_A-Z\d./]+)\] \(([a-zA-Z\d]+)\):/, 112 | titleGroups: { 113 | hashCode: 3, 114 | name: 2, 115 | }, 116 | 117 | entries: { 118 | userId: / userId=(\d+)\b/ 119 | } 120 | } 121 | ] 122 | 123 | // Parses a list of sections 124 | function parseSectionList(parsingEntry, spacesLength) { 125 | let match; 126 | const result = []; 127 | 128 | let spaces = " "; 129 | for (let i = 1; i < spacesLength; i++) { 130 | spaces += " "; 131 | } 132 | 133 | 134 | let lastSection = null; 135 | while(liner.peek() != null && liner.peek().startsWith(spaces)) { 136 | const line = liner.next(); 137 | if (match = parsingEntry.titleRegX.exec(line)) { 138 | if (lastSection != null) { 139 | result.push(lastSection); 140 | } 141 | 142 | lastSection = {hashCode: match[parsingEntry.titleGroups.hashCode], name: match[parsingEntry.titleGroups.name]}; 143 | } else if (lastSection != null) { 144 | for (const [key, value] of Object.entries(parsingEntry.entries)) { 145 | if (match = value.exec(line)) { 146 | lastSection[key] = match[1]; 147 | } 148 | } 149 | } 150 | } 151 | 152 | if (lastSection != null) { 153 | result.push(lastSection); 154 | } 155 | return result; 156 | } 157 | 158 | const parseData = { }; 159 | 160 | let line; 161 | while ((line = liner.next()) != null) { 162 | PARSING_DATA.forEach(p => { 163 | if (p.header == line.trim()) { 164 | const r = parseSectionList(p, line.indexOf(p.header)); 165 | if (!parseData[p.key]) { 166 | parseData[p.key] = []; 167 | } 168 | r.forEach(e => parseData[p.key].push(e)); 169 | } 170 | }) 171 | } 172 | 173 | if (parseData.windows) { 174 | list.forEach(entry => { 175 | parseData.windows.forEach(window => { 176 | if (`${window.hashCode} ${window.name}` == entry.name) { 177 | entry.pid = window.ownerUid; 178 | entry.name = window.name; 179 | if (window.display) { 180 | const parts = window.display.split(","); 181 | entry.display = { 182 | width: parseInt(parts[0]), 183 | height: parseInt(parts[1]) 184 | } 185 | } 186 | if (window.dpi) { 187 | if (!entry.display) { 188 | entry.display = { }; 189 | } 190 | entry.display.density = parseInt(window.dpi) 191 | } 192 | } 193 | }) 194 | 195 | if (parseData.packages) { 196 | parseData.packages.forEach(pkg => { 197 | if (pkg.userId && pkg.userId == entry.pid) { 198 | entry.pname = pkg.name; 199 | } 200 | }) 201 | } 202 | 203 | if (entry.pname && !entry.pname.startsWith("com.android")) { 204 | entry.icon = { 205 | value: `https://cdn.apk-cloud.com/detail/image/${entry.pname}-w250.png` 206 | } 207 | } 208 | }); 209 | } 210 | 211 | list.use_new_api = false; 212 | postMessage({type: TYPE_BUG_REPORT, list: list}); 213 | } 214 | 215 | function newLiner(data /* array buffer */) { 216 | const decoder = new TextDecoder(); 217 | let remaining = data.byteLength; 218 | const chuckSize = 1 << 22; 219 | 220 | let byteStart = 0; 221 | 222 | let lines = [""]; 223 | let linesIndex = 0; 224 | let nextLine; 225 | 226 | function parseNextChunk() { 227 | const length = Math.min(remaining, chuckSize); 228 | const dataView = new DataView(data, byteStart, length); 229 | remaining -= length; 230 | byteStart += chuckSize; 231 | return decoder.decode(dataView).split("\n"); 232 | } 233 | 234 | function consumeNextLine() { 235 | const result = nextLine; 236 | 237 | // Initialize the next line 238 | while (linesIndex >= lines.length - 1 && remaining > 0) { 239 | const lastRow = lines[lines.length - 1]; 240 | lines = parseNextChunk(); 241 | // Merge the very last line with the first line of next chuck 242 | lines[0] = lastRow + lines[0]; 243 | linesIndex = 0; 244 | } 245 | 246 | if (linesIndex >= lines.length) { 247 | nextLine = null; 248 | } else { 249 | nextLine = lines[linesIndex]; 250 | linesIndex++; 251 | } 252 | return result; 253 | } 254 | 255 | // Initialize first chuck 256 | consumeNextLine(); 257 | 258 | return { 259 | peek: function() { 260 | return nextLine; 261 | }, 262 | next: consumeNextLine 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | let progress; 16 | 17 | $(function () { 18 | progress = $("#main-progress"); 19 | 20 | $("#device-picker").click(function () { 21 | handleSelectDevice(navigator.usb.requestDevice({ filters: [ADB_DEVICE_FILTER] })); 22 | }); 23 | 24 | const loadFile = function() { 25 | if (!this.files || this.files.length < 1) { 26 | return; 27 | } 28 | progress.show(); 29 | const w = createWorker("js/file_load_worker.js"); 30 | w.onerror = function(e) { 31 | progress.hide(); 32 | toast("Not a valid view hierarchy file: " + e.message); 33 | }; 34 | w.onmessage = function (e) { 35 | if (e.data.type == TYPE_BUG_REPORT) { 36 | activityListAction(function(callbacks) { 37 | callbacks.windowsLoaded(e.data.list); 38 | }) 39 | } else if (e.data.type == TYPE_ZIP) { 40 | const appInfo = e.data; 41 | appInfo.data = new JSZip(appInfo.data); 42 | hViewAction(appInfo); 43 | } else if (e.data.type == TYPE_ERROR) { 44 | w.onerror(e.data); 45 | } else { 46 | progress.hide(); 47 | toast("Unknown response " + e.data.type); 48 | } 49 | } 50 | w.postMessage(this.files[0]); 51 | } 52 | $("#hierarchy-picker-input").on("change", loadFile); 53 | const pickerButton = $("#hierarchy-picker") 54 | .click(() => $("#hierarchy-picker-input").click()) 55 | .on('dragover dragenter', () => pickerButton.addClass('drag_over')) 56 | .on('dragleave dragend drop', () => pickerButton.removeClass('drag_over')) 57 | .on('drop', e => loadFile.call(e.originalEvent.dataTransfer)) 58 | .on('drag dragstart dragend dragover dragenter dragleave drop', function(e) { 59 | e.preventDefault(); 60 | e.stopPropagation(); 61 | }); 62 | 63 | // Load any verified devices 64 | refreshConnectedDevices(); 65 | navigator.usb.addEventListener("connect", refreshConnectedDevices); 66 | navigator.usb.addEventListener("disconnect", refreshConnectedDevices); 67 | ActiveState.push(function() { 68 | navigator.usb.removeEventListener("connect", refreshConnectedDevices); 69 | navigator.usb.removeEventListener("disconnect", refreshConnectedDevices); 70 | }); 71 | 72 | // Load proxy devices 73 | $("#proxy-handshake-key").val(localStorage.webProxyKey); 74 | trackProxyDevices() 75 | 76 | $("#proxy-copy-command").click(function() { 77 | navigator.clipboard.writeText($("#proxy-copy-command").text()); 78 | toast("Command copied"); 79 | }); 80 | 81 | if (isDarkTheme()) { 82 | switchTheme(); 83 | } 84 | }) 85 | 86 | function refreshConnectedDevices() { 87 | navigator.usb.getDevices().then(devices => { 88 | const container = $("#connected-devices"); 89 | container.empty(); 90 | $("#connected-devices-title")[devices.length == 0 ? "hide" : "show"](); 91 | for (let i = 0; i < devices.length; i++) { 92 | const d = devices[i]; 93 | const entry = $("
").data("device", d).appendTo(container).click(verifiedDeviceClicked).addClass("entry"); 94 | $('
').text(d.manufacturerName + " " + d.productName).appendTo(entry); 95 | 96 | const subText = $('
').appendTo(entry); 97 | $("