├── 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 = $("