├── .gitignore ├── LICENSE ├── README.md ├── build_dist.sh ├── index.html ├── package-lock.json ├── package.json ├── plugins ├── binary-loader.js └── md-loader.js ├── postcss.config.cjs ├── src ├── Extension.vue ├── Nav.vue ├── app │ └── background.js ├── assets │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── fonts │ │ ├── RobotoMono-Regular.ttf │ │ ├── XRXV3I6Li01BKofIMeaBXso.woff2 │ │ ├── XRXV3I6Li01BKofINeaB.woff2 │ │ ├── XRXV3I6Li01BKofIO-aBXso.woff2 │ │ ├── XRXV3I6Li01BKofIOOaBXso.woff2 │ │ ├── XRXV3I6Li01BKofIOuaBXso.woff2 │ │ └── font.css │ ├── icon_64.png │ ├── logo.png │ └── scrcpy-server-v1.24.jar ├── components │ ├── FileManager.vue │ ├── Howto.vue │ ├── Phone.vue │ ├── Scrcpy.vue │ ├── credential-ext.js │ ├── fsutil.js │ └── scrcpyclient.js ├── docs │ ├── enabledebug.md │ ├── privacy.md │ └── terms.md ├── extension.js ├── main.js ├── manifest.json └── style.css ├── tailwind.config.cjs ├── vite.config.js └── vite.extension.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.zip 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | pnpm-debug.log* 9 | lerna-debug.log* 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | sync_www.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Android in web browser 2 | ======= 3 | Using Android phone in web browsers. ([Apache License 2.0](https://github.com/Genymobile/scrcpy/blob/master/LICENSE)) 4 | 5 | 6 | Based: 7 | Vue3 + Vite + TailwindCSS + ya-webadb + scrcpy 8 | 9 | 10 | [Online Demo](https://browserlify.com/?from=github) 11 | 12 | Also availabled in [chrome webstore](https://chrome.google.com/webstore/detail/phone-on-web-browserlifyc/gaibhhiogjpncmcehohlmcikfgbcacbl) 13 | 14 | 15 | ## Features 16 | * Screen mirroring and controling device 17 | * File Management 18 | * Screen Capture 19 | 20 | 21 | ## Enabled USB Debugging in first 22 | * Enabled android phone developer mode 23 | * Enabled USB Debugging mode 24 | 25 | ## How to start 26 | ```shell 27 | # Install depends 28 | npm install --force 29 | 30 | # run local test 31 | npm run dev 32 | 33 | # Bulid for release 34 | npm run build 35 | ``` 36 | 37 | ## Roadmap 38 | * Keyboard mapping 39 | * Screen sharing 40 | * Multi screen in one tab 41 | 42 | ## Credits 43 | * Android Debug Bridge (ADB) for Web Browsers [ya-webadb](https://github.com/yume-chan/ya-webadb) 44 | * Google for [ADB](https://android.googlesource.com/platform/packages/modules/adb) ([Apache License 2.0](./adb.NOTICE)) 45 | * Romain Vimont for [Scrcpy](https://github.com/Genymobile/scrcpy) ([Apache License 2.0](https://github.com/Genymobile/scrcpy/blob/master/LICENSE)) -------------------------------------------------------------------------------- /build_dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -x 3 | echo 'build dist ... ' $@ 4 | rm -Rf dist_$@-$npm_package_version.zip 5 | cd dist 6 | zip -r ../dist_$@-$npm_package_version.zip * -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Android in browser| Phone on web | Browserlify.com 9 | 11 | 12 | 13 | 14 | 15 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "andbrowser", 3 | "private": true, 4 | "version": "1.1.1", 5 | "type": "module", 6 | "scripts": { 7 | "build": "VITE_STORE=www vite build", 8 | "preview": "vite preview", 9 | "dev": "VITE_STORE=dev vite --config vite.extension.config.js", 10 | "edge": "VITE_STORE=edge vite build --config vite.extension.config.js && sh ./build_dist.sh edge", 11 | "chrome": "VITE_STORE=chrome vite build --config vite.extension.config.js && sh ./build_dist.sh chrome" 12 | }, 13 | "dependencies": { 14 | "@heroicons/vue": "^2.0.12", 15 | "@yume-chan/adb": "^0.0.16", 16 | "@yume-chan/adb-backend-webusb": "^0.0.16", 17 | "@yume-chan/adb-credential-web": "^0.0.16", 18 | "@yume-chan/event": "^0.0.16", 19 | "@yume-chan/scrcpy": "^0.0.16", 20 | "@yume-chan/struct": "^0.0.16", 21 | "tinyh264": "^0.0.7", 22 | "vue": "^3.2.41" 23 | }, 24 | "devDependencies": { 25 | "@rollup/plugin-node-resolve": "^15.0.1", 26 | "@vitejs/plugin-legacy": "^2.3.0", 27 | "@vitejs/plugin-vue": "^3.2.0", 28 | "autoprefixer": "^10.4.12", 29 | "markdown-it": "^13.0.1", 30 | "postcss": "^8.4.18", 31 | "tailwindcss": "^3.2.1", 32 | "vite": "^3.2.0", 33 | "vite-plugin-static-copy": "^0.11.1" 34 | } 35 | } -------------------------------------------------------------------------------- /plugins/binary-loader.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | 3 | function toArrayBuffer(base64Data) { 4 | var isBrowser = typeof window !== 'undefined' && typeof window.atob === 'function' 5 | var binary = isBrowser ? window.atob(base64Data) : Buffer.from(base64Data, 'base64').toString('binary') 6 | var bytes = new Uint8Array(binary.length) 7 | 8 | for (var i = 0; i < binary.length; ++i) { 9 | bytes[i] = binary.charCodeAt(i) 10 | } 11 | return bytes.buffer 12 | } 13 | 14 | export function binaryLoader(options = {}) { 15 | const binRegex = /\?binary$/ 16 | return { 17 | name: 'binary-loader', 18 | enforce: 'pre', 19 | 20 | async load(id) { 21 | if (!id.match(binRegex)) { 22 | return 23 | } 24 | const [path, query] = id.split('?', 2) 25 | 26 | let data 27 | try { 28 | data = fs.readFileSync(path) 29 | } catch (ex) { 30 | console.warn(ex, '\n', `${id} couldn't be loaded by binary-loader, fallback to default loader`) 31 | return 32 | } 33 | let base64Data = data.toString('base64') 34 | return `export default (${toArrayBuffer})("${base64Data}");` 35 | } 36 | } 37 | } 38 | 39 | export default binaryLoader -------------------------------------------------------------------------------- /plugins/md-loader.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import { compileTemplate } from '@vue/compiler-sfc' 3 | import MarkdownIt from 'markdown-it' 4 | const __doc__ = ` 5 | ## How to use: 6 | 7 | vite.config.js: 8 | import Markdown from './plugins/md-loader' 9 | export default defineConfig({ 10 | plugins: [ 11 | vue(), 12 | Markdown(), 13 | ], 14 | assetsInclude: ["**/*.md"] 15 | }) 16 | 17 | 18 | in vue: 19 | 20 | import EnableDebugContent from '../docs/enabledebug.md' 21 | 22 | 25 | 26 | ` 27 | export function mdLoader(options = {}) { 28 | const mdRegex = /\.md$/ 29 | return { 30 | name: 'markdown-loader', 31 | enforce: 'pre', 32 | 33 | async load(id) { 34 | if (!id.match(mdRegex)) { 35 | return 36 | } 37 | const [path, query] = id.split('?', 2) 38 | 39 | let data 40 | try { 41 | data = fs.readFileSync(path, 'utf-8') 42 | } catch (ex) { 43 | console.warn(ex, '\n', `${id} couldn't be loaded by vite-md-loader, fallback to default loader`) 44 | return 45 | } 46 | 47 | try { 48 | const md = new MarkdownIt(); 49 | const result = md.render(data); 50 | const { code } = compileTemplate({ 51 | id: JSON.stringify(id), 52 | source: `${result}`, 53 | filename: path, 54 | transformAssetUrls: false 55 | }) 56 | return `${code}\nexport default { render: render }` 57 | } catch (ex) { 58 | console.warn(ex, '\n', `${id} compile markdown fail`) 59 | return 60 | } 61 | } 62 | } 63 | } 64 | 65 | export default mdLoader -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/Extension.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | -------------------------------------------------------------------------------- /src/Nav.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/app/background.js: -------------------------------------------------------------------------------- 1 | chrome.action.onClicked.addListener(() => { 2 | chrome.tabs.create({ 3 | active: true, 4 | url: chrome.runtime.getURL("index.html") 5 | }) 6 | }) -------------------------------------------------------------------------------- /src/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/XRXV3I6Li01BKofIMeaBXso.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/fonts/XRXV3I6Li01BKofIMeaBXso.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/XRXV3I6Li01BKofINeaB.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/fonts/XRXV3I6Li01BKofINeaB.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/XRXV3I6Li01BKofIO-aBXso.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/fonts/XRXV3I6Li01BKofIO-aBXso.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/XRXV3I6Li01BKofIOOaBXso.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/fonts/XRXV3I6Li01BKofIOOaBXso.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/XRXV3I6Li01BKofIOuaBXso.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/fonts/XRXV3I6Li01BKofIOuaBXso.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/font.css: -------------------------------------------------------------------------------- 1 | /* cyrillic-ext */ 2 | @font-face { 3 | font-family: 'Nunito'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url(./XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2'); 7 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 8 | } 9 | /* cyrillic */ 10 | @font-face { 11 | font-family: 'Nunito'; 12 | font-style: normal; 13 | font-weight: 400; 14 | src: url(./XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2'); 15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 16 | } 17 | /* vietnamese */ 18 | @font-face { 19 | font-family: 'Nunito'; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: url(./XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2'); 23 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 24 | } 25 | /* latin-ext */ 26 | @font-face { 27 | font-family: 'Nunito'; 28 | font-style: normal; 29 | font-weight: 400; 30 | src: url(./XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2'); 31 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 32 | } 33 | /* latin */ 34 | @font-face { 35 | font-family: 'Nunito'; 36 | font-style: normal; 37 | font-weight: 400; 38 | src: url(./XRXV3I6Li01BKofINeaB.woff2) format('woff2'); 39 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 40 | } 41 | /* cyrillic-ext */ 42 | @font-face { 43 | font-family: 'Nunito'; 44 | font-style: normal; 45 | font-weight: 700; 46 | src: url(./XRXV3I6Li01BKofIOOaBXso.woff2) format('woff2'); 47 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; 48 | } 49 | /* cyrillic */ 50 | @font-face { 51 | font-family: 'Nunito'; 52 | font-style: normal; 53 | font-weight: 700; 54 | src: url(./XRXV3I6Li01BKofIMeaBXso.woff2) format('woff2'); 55 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 56 | } 57 | /* vietnamese */ 58 | @font-face { 59 | font-family: 'Nunito'; 60 | font-style: normal; 61 | font-weight: 700; 62 | src: url(./XRXV3I6Li01BKofIOuaBXso.woff2) format('woff2'); 63 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB; 64 | } 65 | /* latin-ext */ 66 | @font-face { 67 | font-family: 'Nunito'; 68 | font-style: normal; 69 | font-weight: 700; 70 | src: url(./XRXV3I6Li01BKofIO-aBXso.woff2) format('woff2'); 71 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 72 | } 73 | /* latin */ 74 | @font-face { 75 | font-family: 'Nunito'; 76 | font-style: normal; 77 | font-weight: 700; 78 | src: url(./XRXV3I6Li01BKofINeaB.woff2) format('woff2'); 79 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; 80 | } -------------------------------------------------------------------------------- /src/assets/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/icon_64.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/scrcpy-server-v1.24.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restsend/andbrowser/9ef5603244e555b5cb258618f56fd04cf73f0511/src/assets/scrcpy-server-v1.24.jar -------------------------------------------------------------------------------- /src/components/FileManager.vue: -------------------------------------------------------------------------------- 1 | 114 | -------------------------------------------------------------------------------- /src/components/Howto.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/Phone.vue: -------------------------------------------------------------------------------- 1 | 195 | -------------------------------------------------------------------------------- /src/components/Scrcpy.vue: -------------------------------------------------------------------------------- 1 | 196 | -------------------------------------------------------------------------------- /src/components/credential-ext.js: -------------------------------------------------------------------------------- 1 | // cspell: ignore RSASSA 2 | 3 | import { calculateBase64EncodedLength, calculatePublicKey, calculatePublicKeyLength, decodeBase64, decodeUtf8, encodeBase64 } from "@yume-chan/adb"; 4 | 5 | export default class AdbWebCredentialExtStore { 6 | 7 | constructor(localStorageKey = 'private-key') { 8 | this.localStorageKey = localStorageKey; 9 | this._vals = {} 10 | } 11 | 12 | setItem(key, value) { 13 | return new Promise((solve, reject) => { 14 | let vals = {} 15 | vals[key] = value 16 | 17 | if (chrome.storage) { 18 | chrome.storage.local.set(vals, () => { 19 | solve() 20 | }); 21 | } else { 22 | this._vals[key] = value 23 | solve() 24 | } 25 | }) 26 | } 27 | 28 | getItem(key) { 29 | return new Promise((solve, reject) => { 30 | if (chrome.storage) { 31 | chrome.storage.local.get(key, (vals) => { 32 | if (vals[key]) { 33 | solve(vals[key]) 34 | } else { 35 | solve(undefined) 36 | } 37 | }); 38 | } else { 39 | solve(this._vals[key]) 40 | } 41 | }) 42 | } 43 | 44 | async *iterateKeys() { 45 | const privateKey = await this.getItem(this.localStorageKey); 46 | if (privateKey) { 47 | yield decodeBase64(privateKey); 48 | } 49 | } 50 | 51 | async generateKey() { 52 | const { privateKey: cryptoKey } = await crypto.subtle.generateKey( 53 | { 54 | name: 'RSASSA-PKCS1-v1_5', 55 | modulusLength: 2048, 56 | // 65537 57 | publicExponent: new Uint8Array([0x01, 0x00, 0x01]), 58 | hash: 'SHA-1', 59 | }, 60 | true, 61 | ['sign', 'verify'] 62 | ); 63 | 64 | const privateKey = new Uint8Array(await crypto.subtle.exportKey('pkcs8', cryptoKey)); 65 | await this.setItem(this.localStorageKey, decodeUtf8(encodeBase64(privateKey))); 66 | 67 | // The authentication module in core doesn't need public keys. 68 | // It will generate the public key from private key every time. 69 | // However, maybe there are people want to manually put this public key onto their device, 70 | // so also save the public key for their convenience. 71 | const publicKeyLength = calculatePublicKeyLength(); 72 | const [publicKeyBase64Length] = calculateBase64EncodedLength(publicKeyLength); 73 | const publicKeyBuffer = new Uint8Array(publicKeyBase64Length); 74 | calculatePublicKey(privateKey, publicKeyBuffer); 75 | encodeBase64( 76 | publicKeyBuffer.subarray(0, publicKeyLength), 77 | publicKeyBuffer 78 | ); 79 | await this.setItem(this.localStorageKey + '.pub', decodeUtf8(publicKeyBuffer)); 80 | 81 | return privateKey; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/components/fsutil.js: -------------------------------------------------------------------------------- 1 | import { 2 | ChunkStream, 3 | ADB_SYNC_MAX_PACKET_SIZE, 4 | LinuxFileType, 5 | WrapReadableStream 6 | } from "@yume-chan/adb"; 7 | 8 | export class FileManager { 9 | constructor(device) { 10 | this.device = device 11 | } 12 | 13 | joinPath(cwd, name) { 14 | if (cwd[cwd.length - 1] == '/') { 15 | return cwd + name 16 | } else { 17 | return cwd + '/' + name 18 | } 19 | } 20 | 21 | async openDir(currentPath) { 22 | let cwd = currentPath || '/' 23 | const sync = await this.device.sync(); 24 | let filelist = [] 25 | if (cwd != '/') { 26 | let vals = cwd.split('/') 27 | filelist.push({ 28 | type: LinuxFileType.Directory, 29 | name: '..', 30 | path: vals.slice(0, vals.length - 1).join('/') 31 | }) 32 | } 33 | 34 | try { 35 | for await (const entry of sync.opendir(cwd)) { 36 | if (entry.name === '.' || entry.name === '..') { 37 | continue; 38 | } 39 | 40 | filelist.push({ 41 | name: entry.name, 42 | path: this.joinPath(cwd, entry.name), 43 | size: Number(entry.size), 44 | type: entry.type, 45 | mtime: Number(entry.mtime) 46 | }) 47 | } 48 | } catch (e) { 49 | sync.dispose() 50 | throw e 51 | } 52 | try { 53 | for (let idx = 0; idx < filelist.length; idx++) { 54 | const item = filelist[idx]; 55 | if (item.type == LinuxFileType.Link) { 56 | const isDir = await sync.isDirectory(item.path) 57 | item.type = isDir ? LinuxFileType.Directory : LinuxFileType.File 58 | filelist[idx] = item 59 | } 60 | } 61 | } catch (e) { 62 | // ignore 63 | } 64 | 65 | sync.dispose() 66 | return filelist 67 | } 68 | 69 | async openFile(item) { 70 | if (item.type == LinuxFileType.Directory) { return } 71 | const sync = await this.device.sync(); 72 | let data = [] 73 | 74 | try { 75 | let stream = await sync.read(item.path) 76 | await stream.pipeTo(new WritableStream({ 77 | write: (v) => { 78 | data.push(v) 79 | }, 80 | })) 81 | } catch (e) { 82 | sync.dispose() 83 | throw e 84 | } 85 | sync.dispose() 86 | return new Blob(data) 87 | } 88 | 89 | async deleteFile(itemPath) { 90 | await this.device.rm(itemPath); 91 | } 92 | 93 | async uploadFile(itemPath, file) { 94 | const sync = await this.device.sync(); 95 | try { 96 | await new WrapReadableStream(file.stream()) 97 | .pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE)) 98 | .pipeTo(sync.write( 99 | itemPath, 100 | (LinuxFileType.File << 12) | 0o666, 101 | file.lastModified / 1000, 102 | )); 103 | } catch (e) { 104 | sync.dispose() 105 | throw e 106 | } 107 | sync.dispose() 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/components/scrcpyclient.js: -------------------------------------------------------------------------------- 1 | import { 2 | Adb, 3 | ChunkStream, 4 | WrapReadableStream, 5 | InspectStream, 6 | ADB_SYNC_MAX_PACKET_SIZE 7 | } from "@yume-chan/adb"; 8 | 9 | import { 10 | DEFAULT_SERVER_PATH, 11 | ScrcpyClient, 12 | ScrcpyOptions1_24, 13 | CodecOptions, 14 | pushServer, 15 | ScrcpyLogLevel, 16 | WebCodecsDecoder, 17 | ScrcpyVideoOrientation, 18 | AndroidKeyCode, 19 | AndroidKeyEventAction, 20 | AndroidMotionEventAction 21 | } from "@yume-chan/scrcpy"; 22 | 23 | import AdbWebUsbBackend from "@yume-chan/adb-backend-webusb"; 24 | import AdbWebCredentialStore from "@yume-chan/adb-credential-web"; 25 | import AdbWebCredentialExtStore from "./credential-ext" 26 | import Struct from "@yume-chan/struct" 27 | 28 | import { FileManager } from "./fsutil"; 29 | 30 | const SCRCPY_SERVER_VERSION = "1.24"; 31 | /* 32 | import SCRCPY_SERVER_URL from "../assets/scrcpy-server-v1.24.jar"; 33 | await fetch(SCRCPY_SERVER_URL) 34 | .then(response => new WrapReadableStream(response.body)) 35 | .then(stream => stream.pipeTo(pushServer(this.device))); 36 | */ 37 | 38 | import SCRCPY_SERVER_BIN from "../assets/scrcpy-server-v1.24.jar?binary"; 39 | 40 | const SCREEN_POWER_MODE_OFF = 0; 41 | const SCREEN_POWER_MODE_NORMAL = 2; 42 | const SetScreenPowerMode = 10; 43 | 44 | function clamp(value, min, max) { 45 | if (value < min) { 46 | return min; 47 | } 48 | 49 | if (value > max) { 50 | return max; 51 | } 52 | return value; 53 | } 54 | const ScrcpySetPowerModeControlMessage = new Struct() 55 | .uint8('type') 56 | .uint8('mode') 57 | 58 | class KeyRepeater { 59 | constructor(key, client, delay = 0, interval = 0) { 60 | this.key = key; 61 | this.client = client; 62 | 63 | this.delay = delay; 64 | this.interval = interval; 65 | } 66 | 67 | async press() { 68 | await this.client.injectKeyCode({ 69 | action: AndroidKeyEventAction.Down, 70 | keyCode: this.key, 71 | repeat: 0, 72 | metaState: 0, 73 | }); 74 | 75 | if (this.delay === 0) { 76 | return; 77 | } 78 | 79 | const timeoutId = setTimeout(async () => { 80 | await this.client.injectKeyCode({ 81 | action: AndroidKeyEventAction.Down, 82 | keyCode: this.key, 83 | repeat: 1, 84 | metaState: 0, 85 | }); 86 | 87 | if (this.interval === 0) { 88 | return; 89 | } 90 | 91 | const intervalId = setInterval(async () => { 92 | await this.client.injectKeyCode({ 93 | action: AndroidKeyEventAction.Down, 94 | keyCode: this.key, 95 | repeat: 1, 96 | metaState: 0, 97 | }); 98 | }, this.interval); 99 | this.onRelease = () => clearInterval(intervalId); 100 | }, this.delay); 101 | this.onRelease = () => clearTimeout(timeoutId); 102 | } 103 | 104 | async release() { 105 | if (this.onRelease) { 106 | this.onRelease(); 107 | } 108 | 109 | await this.client.injectKeyCode({ 110 | action: AndroidKeyEventAction.Up, 111 | keyCode: this.key, 112 | repeat: 0, 113 | metaState: 0, 114 | }); 115 | } 116 | } 117 | 118 | 119 | class ScpyClient { 120 | constructor() { 121 | this.deviceName = '' 122 | this.canvas = undefined 123 | this.parent = undefined 124 | this.device = undefined 125 | this.credentialStore = undefined 126 | 127 | if (typeof chrome !== "undefined" && chrome.runtime != undefined) { 128 | this.credentialStore = new AdbWebCredentialExtStore() 129 | } else { 130 | this.credentialStore = new AdbWebCredentialStore(); 131 | } 132 | 133 | this.bitRatesCount = 0 134 | this.bitRatesTimerId = setInterval(() => { 135 | this.bitRatesCount = 0; 136 | }, 1000); 137 | this.decoder = undefined; 138 | this.logLevel = ScrcpyLogLevel.Debug; 139 | this.maxSize = 0 140 | this.bitRate = 8_000_000 141 | this.client = undefined 142 | 143 | this.homeKeyRepeater = undefined 144 | this.appSwitchKeyRepeater = undefined 145 | this.screenOn = undefined 146 | 147 | this.supported = AdbWebUsbBackend.isSupported(); 148 | this.onVideoResize = (width, height) => { } 149 | } 150 | 151 | get connected() { 152 | // return true 153 | return this.client != undefined 154 | } 155 | 156 | createCanvas(parent) { 157 | if (this.decoder) { 158 | this.canvas = undefined; 159 | parent.removeChild(this.decoder.renderer) 160 | } 161 | this.parent = parent 162 | 163 | this.decoder = new WebCodecsDecoder(); 164 | parent.appendChild(this.decoder.renderer); 165 | this.canvas = this.decoder.renderer; 166 | this.processEvents() 167 | } 168 | 169 | addlog(text) { 170 | let elm = document.querySelector("#logs"); 171 | if (!elm) { 172 | return 173 | } 174 | let logElm = document.createElement("p"); 175 | logElm.innerText = `${new Date().toLocaleString()} ${text}`; 176 | elm.appendChild(logElm); 177 | } 178 | 179 | async pickDevice() { 180 | let cacheDevices = (await AdbWebUsbBackend.getDevices()) || []; 181 | let deviceMeta = undefined; 182 | if (cacheDevices.length > 0) { 183 | deviceMeta = cacheDevices[0]; 184 | } else { 185 | deviceMeta = await AdbWebUsbBackend.requestDevice(); 186 | } 187 | return deviceMeta 188 | } 189 | 190 | async connectDevice(deviceMeta) { 191 | if (!this.device) { 192 | this.disconnect() 193 | } 194 | 195 | let streams = await deviceMeta.connect(); 196 | this.addlog(`[client] authenticate ${deviceMeta.serial}`); 197 | 198 | this.device = await Adb.authenticate( 199 | streams, 200 | this.credentialStore, 201 | undefined 202 | ); 203 | 204 | this.device.disconnected.then(() => { 205 | this.addlog("[client] disconnected done"); 206 | }); 207 | 208 | this.deviceName = deviceMeta.name 209 | return this.device; 210 | } 211 | 212 | async disconnect() { 213 | if (this.client) { 214 | this.client.close() 215 | this.client = undefined 216 | } 217 | if (this.device) { 218 | this.device.close() 219 | this.device = undefined 220 | } 221 | 222 | if (this.decoder && this.parent) { 223 | this.parent.removeChild(this.decoder.renderer) 224 | this.decoder.dispose() 225 | this.decoder = undefined 226 | this.parent = undefined 227 | this.canvas = undefined 228 | } 229 | } 230 | 231 | async connectScrcpy(parent) { 232 | 233 | await new WrapReadableStream({ 234 | start(ctrl) { 235 | ctrl.enqueue(new Uint8Array(SCRCPY_SERVER_BIN)); 236 | ctrl.close(); 237 | }, 238 | }) 239 | .pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE)) 240 | .pipeTo(pushServer(this.device)); 241 | 242 | 243 | this.createCanvas(parent) 244 | 245 | const options = new ScrcpyOptions1_24({ 246 | logLevel: this.logLevel, 247 | maxSize: this.maxSize, 248 | bitRate: this.bitRate, 249 | lockVideoOrientation: ScrcpyVideoOrientation.Unlocked, 250 | tunnelForward: false, 251 | sendDeviceMeta: false, 252 | sendDummyByte: false, 253 | codecOptions: new CodecOptions({ 254 | profile: this.decoder.maxProfile, 255 | level: this.decoder.maxLevel, 256 | }), 257 | }); 258 | 259 | this.addlog(`[client] Server arguments: ${options 260 | .formatServerArguments() 261 | .join(" ")}`); 262 | 263 | this.client = await ScrcpyClient.start( 264 | this.device, 265 | DEFAULT_SERVER_PATH, 266 | SCRCPY_SERVER_VERSION, 267 | options 268 | ); 269 | 270 | this.client.stdout.pipeTo( 271 | new WritableStream({ 272 | write: (line) => { 273 | this.addlog(`${line}`); 274 | }, 275 | }) 276 | ); 277 | 278 | this.client.videoStream 279 | .pipeThrough( 280 | new InspectStream((packet) => { 281 | if (packet.type === "configuration") { 282 | const { 283 | croppedWidth, 284 | croppedHeight 285 | } = packet.data; 286 | 287 | this.width = croppedWidth 288 | this.height = croppedHeight 289 | this.onVideoResize(this.width, this.height); 290 | 291 | this.addlog( 292 | `[client] Video size changed: ${croppedWidth}x${croppedHeight}` 293 | ); 294 | } else { 295 | this.bitRatesCount += packet.data.length; 296 | } 297 | }) 298 | ) 299 | .pipeTo(this.decoder.writable); 300 | 301 | this.homeKeyRepeater = new KeyRepeater(AndroidKeyCode.Home, this.client); 302 | this.appSwitchKeyRepeater = new KeyRepeater(AndroidKeyCode.AppSwitch, this.client); 303 | this.screenOn = undefined 304 | await this.toggleScreen() 305 | } 306 | 307 | calculatePointerPosition(clientX, clientY) { 308 | const view = this.canvas.getBoundingClientRect() 309 | const pointerViewX = clientX - view.x 310 | const pointerViewY = clientY - view.y 311 | const pointerScreenX = clamp(pointerViewX / view.width, 0, 1) * this.width 312 | const pointerScreenY = clamp(pointerViewY / view.height, 0, 1) * this.height 313 | 314 | return { 315 | x: pointerScreenX, 316 | y: pointerScreenY, 317 | }; 318 | } 319 | 320 | async injectTouch(action, e) { 321 | if (!this.client) { 322 | return; 323 | } 324 | 325 | const { 326 | x, 327 | y 328 | } = this.calculatePointerPosition(e.clientX, e.clientY); 329 | 330 | await this.client.injectTouch({ 331 | action, 332 | pointerId: e.pointerType === "mouse" ? BigInt(-1) : BigInt(e.pointerId), 333 | pointerX: x, 334 | pointerY: y, 335 | pressure: e.pressure * 65535, 336 | buttons: e.buttons, 337 | }); 338 | }; 339 | 340 | async handlePointerDown(e) { 341 | if (!this.client) { 342 | return 343 | } 344 | this.parent.focus(); 345 | e.preventDefault(); 346 | e.currentTarget.setPointerCapture(e.pointerId); 347 | await this.injectTouch(AndroidMotionEventAction.Down, e); 348 | } 349 | 350 | async handlePointerUp(e) { 351 | if (!this.client) { 352 | return 353 | } 354 | await this.injectTouch(AndroidMotionEventAction.Up, e); 355 | } 356 | 357 | async handlePointerMove(e) { 358 | await this.injectTouch( 359 | e.buttons === 0 ? AndroidMotionEventAction.HoverMove : AndroidMotionEventAction.Move, 360 | e 361 | ); 362 | } 363 | 364 | async handleWheel(e) { 365 | if (!this.client) { 366 | return 367 | } 368 | e.preventDefault(); 369 | e.stopPropagation(); 370 | 371 | const { 372 | x, 373 | y 374 | } = this.calculatePointerPosition(e.clientX, e.clientY); 375 | this.client.injectScroll({ 376 | pointerX: x, 377 | pointerY: y, 378 | scrollX: -Math.sign(e.deltaX), 379 | scrollY: -Math.sign(e.deltaY), 380 | buttons: 0, 381 | }); 382 | } 383 | 384 | async handleBackPointerDown(e) { 385 | if (e.button !== 0 || !this.client) { 386 | return; 387 | } 388 | e.currentTarget.setPointerCapture(e.pointerId); 389 | await this.client.pressBackOrTurnOnScreen(AndroidKeyEventAction.Down); 390 | } 391 | 392 | async handleBackPointerUp(e) { 393 | if (e.button !== 0 || !this.client) { 394 | return; 395 | } 396 | await this.client.pressBackOrTurnOnScreen(AndroidKeyEventAction.Up); 397 | } 398 | 399 | async handleHomePointerDown(e) { 400 | if (e.button !== 0 || !this.client) { 401 | return; 402 | } 403 | e.currentTarget.setPointerCapture(e.pointerId); 404 | await this.homeKeyRepeater.press(); 405 | } 406 | 407 | async handleHomePointerUp(e) { 408 | if (e.button !== 0 || !this.client) { 409 | return; 410 | } 411 | await this.homeKeyRepeater.release(); 412 | } 413 | 414 | async handleAppSwitchPointerDown(e) { 415 | if (e.button !== 0 || !this.client) { 416 | return; 417 | } 418 | e.currentTarget.setPointerCapture(e.pointerId); 419 | await this.appSwitchKeyRepeater.press(); 420 | } 421 | 422 | async handleAppSwitchPointerUp(e) { 423 | if (e.button !== 0 || !this.client) { 424 | return; 425 | } 426 | await this.appSwitchKeyRepeater.release(); 427 | } 428 | 429 | async toggleScreen() { 430 | if (!this.client) { 431 | return true; 432 | } 433 | 434 | let mode = SCREEN_POWER_MODE_OFF 435 | if (!this.screenOn) { 436 | mode = SCREEN_POWER_MODE_NORMAL 437 | this.screenOn = true 438 | } else { 439 | this.screenOn = false 440 | } 441 | 442 | const controlStream = this.client.checkControlStream('setScreenPowerMode'); 443 | const buffer = ScrcpySetPowerModeControlMessage.serialize({ 444 | type: SetScreenPowerMode, 445 | mode, 446 | }); 447 | 448 | if (buffer) { 449 | await controlStream.write(buffer); 450 | } 451 | return this.screenOn 452 | } 453 | 454 | getCanvas() { 455 | if (!this.client) { 456 | return 457 | } 458 | return this.canvas 459 | } 460 | 461 | getFileManager() { 462 | if (!this.client) { 463 | return 464 | } 465 | return new FileManager(this.device) 466 | } 467 | 468 | async handleKey(e) { 469 | if (!this.client) { 470 | return 471 | } 472 | 473 | e.preventDefault(); 474 | 475 | const { 476 | key 477 | } = e; 478 | switch (key) { 479 | case "Escape": 480 | await this.client.pressBackOrTurnOnScreen(AndroidKeyEventAction.Down); 481 | await this.client.pressBackOrTurnOnScreen(AndroidKeyEventAction.Up); 482 | return 483 | case "Enter": 484 | case "Shift": 485 | case "Control": 486 | case "Alt": 487 | return 488 | } 489 | 490 | const keyCode = { 491 | Backspace: AndroidKeyCode.Delete, 492 | Space: AndroidKeyCode.Space, 493 | }[key] 494 | 495 | if (keyCode) { 496 | await this.client.injectKeyCode({ 497 | action: AndroidKeyEventAction.Down, 498 | keyCode, 499 | metaState: 0, 500 | repeat: 0, 501 | }); 502 | await this.client.injectKeyCode({ 503 | action: AndroidKeyEventAction.Up, 504 | keyCode, 505 | metaState: 0, 506 | repeat: 0, 507 | }); 508 | } else { 509 | this.client.injectText(key); 510 | } 511 | } 512 | 513 | processEvents() { 514 | this.emitKey = (evt) => { 515 | this.handleKey(evt) 516 | } 517 | this.bindKeyEvents = () => { 518 | this.canvas.focus(); 519 | document.body.addEventListener('keydown', this.emitKey, true); 520 | } 521 | 522 | this.unbindKeyEvents = () => { 523 | document.body.removeEventListener('keydown', this.emitKey, true); 524 | } 525 | 526 | this.canvas.addEventListener('pointerdown', e => { 527 | return this.handlePointerDown(e) 528 | }, false); 529 | this.canvas.addEventListener('pointerup', e => { 530 | return this.handlePointerUp(e) 531 | }, false); 532 | this.canvas.addEventListener('pointermove', e => { 533 | return this.handlePointerMove(e) 534 | }, false); 535 | this.canvas.addEventListener('contextmenu', e => { 536 | e.preventDefault(); 537 | }, false); 538 | this.canvas.addEventListener('wheel', e => { 539 | return this.handleWheel(e); 540 | }, false); 541 | 542 | this.canvas.addEventListener('mouseenter', e => { 543 | this.bindKeyEvents() 544 | }, false); 545 | this.canvas.addEventListener('mouseleave', e => { 546 | this.unbindKeyEvents() 547 | }, false); 548 | } 549 | 550 | 551 | } 552 | const client = new ScpyClient() 553 | export default client -------------------------------------------------------------------------------- /src/docs/enabledebug.md: -------------------------------------------------------------------------------- 1 | Need to __turn on USB debugging mode__ on your phone and link the __USB cable__ to your computer 2 | 3 | Step 1: Tap Menu button so that you can enter into App drawer. 4 | 5 | Step 2: Access __Settings__ and tap __About phone__ which you can find at the bottom of setting interface. 6 | 7 | Step 3: In the __About phone__ interface, you will see __Build Number__ option at its bottom. Tap it continuously __until__ you see the countdown “You are now a developer” 8 | 9 | Step 4: Click the Back button and you can see the Developer options menu is activated. 10 | 11 | Step 5: Now you can turn __USB debugging__ on and then tap __OK__ to allow USB debugging. 12 | 13 | Step 6: Now, plug in your Android device using the USB cable. 14 | 15 | Step 7: __Allow USB Debugging confirmation on your phone.__ 16 | 17 | Step 8: Select Your phone on Edge/Chrome confrim window, press __CONNECT__ to start. -------------------------------------------------------------------------------- /src/docs/privacy.md: -------------------------------------------------------------------------------- 1 | ### Privacy Policy 2 | 3 | Your privacy is important to us. It is ForGrowth Inc.'s policy to respect your privacy regarding any information we may collect from you across our website, For Growth, and other sites we own and operate. 4 | 5 | #### Information we collect 6 | 7 | ##### Log data 8 | When you visit our website, our servers may automatically log the standard data provided by your web browser. It may include your computer’s Internet Protocol (IP) address, your browser type and version, the pages you visit, the time and date of your visit, the time spent on each page, and other details. 9 | 10 | ##### Device data 11 | We may also collect data about the device you’re using to access our website. This data may include the device type, operating system, unique device identifiers, device settings, and geo-location data. What we collect can depend on the individual settings of your device and software. We recommend checking the policies of your device manufacturer or software provider to learn what information they make available to us. 12 | 13 | ##### Personal information 14 | We may ask for personal information, such as your: 15 | ```shell 16 | Name 17 | Email 18 | Phone/mobile number 19 | Home/Mailing address 20 | Work address 21 | Website address 22 | Payment information 23 | ``` 24 | 25 | ##### Business data 26 | Business data refers to data that accumulates over the normal course of operation on our platform. This may include transaction records, stored files, user profiles, analytics data and other metrics, as well as other types of information, created or generated, as users interact with our services. 27 | 28 | #### Legal bases for processing 29 | 30 | We will process your personal information lawfully, fairly and in a transparent manner. We collect and process information about you only where we have legal bases for doing so. 31 | 32 | These legal bases depend on the services you use and how you use them, meaning we collect and use your information only where: 33 | 34 | it’s necessary for the performance of a contract to which you are a party or to take steps at your request before entering into such a contract (for example, when we provide a service you request from us); 35 | 36 | it satisfies a legitimate interest (which is not overridden by your data protection interests), such as for research and development, to market and promote our services, and to protect our legal rights and interests; 37 | 38 | you give us consent to do so for a specific purpose; or we need to process your data to comply with a legal obligation. 39 | 40 | Where you consent to our use of information about you for a specific purpose, you have the right to change your mind at any time (but this will not affect any processing that has already taken place). 41 | 42 | We don’t keep personal information for longer than is necessary. While we retain this information, we will protect it within commercially acceptable means to prevent loss and theft, as well as unauthorized access, disclosure, copying, use or modification. That said, we advise that no method of electronic transmission or storage is 100% secure and cannot guarantee absolute data security. If necessary, we may retain your personal information for our compliance with a legal obligation or in order to protect your vital interests or the vital interests of another natural person. 43 | 44 | #### Collection and use of information 45 | 46 | We may collect, hold, use and disclose information for the following purposes and personal information will not be further processed in a manner that is incompatible with these purposes: 47 | 48 | - to provide you with our platform's core features; 49 | - to process any transactional or ongoing payments; 50 | - to enable you to access and use our website, associated applications and associated social media platforms; 51 | - to contact and communicate with you; 52 | - for internal record keeping and administrative purposes; 53 | - for analytics, market research and business development, including to operate and improve our website, associated applications and associated social media platforms; 54 | for advertising and marketing, including to send you promotional information about our products and services and information about third parties that we consider may be of interest to you; 55 | - to comply with our legal obligations and resolve any disputes that we may have; and 56 | - to consider your employment application. 57 | 58 | #### Disclosure of personal information to third parties 59 | 60 | We may disclose personal information to: 61 | 62 | - third party service providers for the purpose of enabling them to provide their services, including (without limitation) IT service providers, data storage, hosting and server providers, ad networks, analytics, error loggers, debt collectors, maintenance or problem-solving providers, marketing or advertising providers, professional advisors and payment systems operators; 63 | - our employees; 64 | - credit reporting agencies, courts, tribunals and regulatory authorities, in the event you fail to pay for goods or services we have provided to you; 65 | - courts, tribunals, regulatory authorities and law enforcement officers, as required by law, in connection with any actual or prospective legal proceedings, or in order to establish, - exercise or defend our legal rights; 66 | - third parties to collect and process data. 67 | 68 | #### International transfers of personal information 69 | 70 | The personal information we collect is stored and processed where we , providers maintain facilities. By providing us with your personal information, you consent to the disclosure to these third parties. 71 | 72 | We will ensure that any transfer of personal information from countries in the European Economic Area (EEA) to countries outside the EEA will be protected by appropriate safeguards, for example by using standard data protection clauses approved by the European Commission, or the use of binding corporate rules or other legally accepted means. 73 | 74 | Where we transfer personal information from a non-EEA country to another country, you acknowledge that third parties in other jurisdictions may not be subject to similar data protection laws to the ones in our jurisdiction. There are risks if any such third party engages in any act or practice that would contravene the data privacy laws in our jurisdiction and this might mean that you will not be able to seek redress under our jurisdiction’s privacy laws. 75 | 76 | #### Your rights and controlling your personal information 77 | 78 | ##### Choice and consent 79 | By providing personal information to us, you consent to us collecting, holding, using and disclosing your personal information in accordance with this privacy policy. If you are under 16 years of age, you must have, and warrant to the extent permitted by law to us, that you have your parent or legal guardian’s permission to access and use the website and they (your parents or guardian) have consented to you providing us with your personal information. You do not have to provide personal information to us, however, if you do not, it may affect your use of this website or the products and/or services offered on or through it. 80 | 81 | ##### Information from third parties 82 | If we receive personal information about you from a third party, we will protect it as set out in this privacy policy. If you are a third party providing personal information about somebody else, you represent and warrant that you have such person’s consent to provide the personal information to us. 83 | 84 | ##### Restrict 85 | You may choose to restrict the collection or use of your personal information. If you have previously agreed to us using your personal information for direct marketing purposes, you may change your mind at any time by contacting us using the details below. If you ask us to restrict or limit how we process your personal information, we will let you know how the restriction affects your use of our website or products and services. 86 | 87 | ##### Access and data portability 88 | You may request details of the personal information that we hold about you. You may request a copy of the personal information we hold about you. Where possible, we will provide this information in CSV format or other easily readable machine format. You may request that we erase the personal information we hold about you at any time. You may also request that we transfer this personal information to another third party. 89 | 90 | ##### Correction 91 | If you believe that any information we hold about you is inaccurate, out of date, incomplete, irrelevant or misleading, please contact us using the details below. We will take reasonable steps to correct any information found to be inaccurate, incomplete, misleading or out of date. 92 | 93 | ##### Notification of data breaches 94 | We will comply laws applicable to us in respect of any data breach. 95 | 96 | ##### Complaints 97 | If you believe that we have breached a relevant data protection law and wish to make a complaint, please contact us using the details below and provide us with full details of the alleged breach. We will promptly investigate your complaint and respond to you, in writing, setting out the outcome of our investigation and the steps we will take to deal with your complaint. You also have the right to contact a regulatory body or data protection authority in relation to your complaint. 98 | 99 | ##### Unsubscribe 100 | To unsubscribe from our e-mail database or opt-out of communications (including marketing communications), please contact us using the details below or opt-out using the opt-out facilities provided in the communication. 101 | 102 | #### Cookies 103 | 104 | We use “cookies” to collect information about you and your activity across our site. A cookie is a small piece of data that our website stores on your computer, and accesses each time you visit, so we can understand how you use our site. This helps us serve you content based on preferences you have specified. Please refer to our Cookie Policy for more information. 105 | 106 | #### Business transfers 107 | 108 | If we or our assets are acquired, or in the unlikely event that we go out of business or enter bankruptcy, we would include data among the assets transferred to any parties who acquire us. You acknowledge that such transfers may occur, and that any parties who acquire us may continue to use your personal information according to this policy. 109 | 110 | #### Changes to this policy 111 | 112 | At our discretion, we may change our privacy policy to reflect current acceptable practices. We will take reasonable steps to let users know about changes via our website. Your continued use of this site after any changes to this policy will be regarded as acceptance of our practices around privacy and personal information. 113 | 114 | If we make a significant change to this privacy policy, for example changing a lawful basis on which we process your personal information, we will ask you to re-consent to the amended privacy policy. 115 | 116 | z-yun@fourz.cn 117 | 118 | This policy is effective as of 30 April 2022. -------------------------------------------------------------------------------- /src/docs/terms.md: -------------------------------------------------------------------------------- 1 | ### Terms and Conditions 2 | 3 | Please read these Terms and Conditions ("Terms", "Terms and Conditions") carefully before using the https://browserlify.com website and all operated by ForGrowth. 4 | 5 | The following terminology applies to these Terms and Conditions, Privacy Statement and Disclaimer Notice and any or all Agreements: 6 | 7 | “You” and “Your” refers to the user accessing this website and accepting the Company’s terms and conditions. 8 | 9 | “Ourselves”, “We”, “Our” and "Us", refers to ForGrowth. 10 | 11 | #### Acceptance of ForGrowth Terms and Conditions of Service 12 | 13 | * By visiting our site and/ or purchasing something from us, you engage in our “Service”. 14 | Your access to and use of the Service is conditioned on your acceptance of and compliance with these Terms. 15 | * These Terms apply to all visitors, users and others who access or use the Service. 16 | * By accessing or using the Service you agree to be bound by these Terms. If you disagree with any part of the terms then you may not access the Service. 17 | 18 | #### Credit Card Details 19 | 20 | ForGrowth will not ask for your credit card details, except for when subscribing to the ForGrowth recurring payment subscription service. 21 | We advise our customers to not enter their credit cards details in the Service or by submitting such details in any other form if you are not setting up your ForGrowth recurring payment subscription service. 22 | If you are unsure we urge you to contact us at hello@browserlify.com . ForGrowth can not be held responsible for any fraud or phishing attempts 23 | 24 | #### Change of Use 25 | 26 | ForGrowth reserves the right, at our sole discretion, to: 27 | * change, modify or remove (temporarily or permanently) the Service or any part of it without notice and you confirm that the Service shall not be liable to you for any such change or removal. 28 | * change these Terms and Conditions at any time. 29 | * change the price for the Service without notice and your continued use of the Service following any changes shall be deemed to be your acceptance of such change. 30 | 31 | #### Links to, and use of, Third Party 32 | 33 | * ForGrowth may include links to, or make use of, third party software and services that are controlled and maintained by others (hereby referred to only as Third Party). 34 | * Any link to or usage of Third Party is not an endorsement of Third Party. 35 | * You acknowledge and agree that we are not responsible for the content or availability of any such Third Party. 36 | 37 | #### Copyright 38 | 39 | * All copyright, trade-marks and all other intellectual property rights of ForGrowth and the Service (including without limitation the design, text, graphics and all software and source codes connected with ForGrowth are owned by or licensed to ForGrowth or otherwise used by ForGrowth as permitted by law.) 40 | * In accessing ForGrowth you agree that you will access the Service solely for your personal, non-commercial use.[b] None of the content may be downloaded, copied, reproduced, transmitted, stored, sold or distributed without the prior written consent of the copyright holder. 41 | 42 | #### Disclaimers and Liability 43 | 44 | * Use of the Service is at your own risk. Our Service is provided on an AS IS and AS AVAILABLE basis without any representation or endorsement made and without warranty of any kind whether express or implied, including but not limited to the implied warranties of satisfactory quality, fitness for a particular purpose, non-infringement, compatibility, security and accuracy. 45 | * To the extent permitted by law, ForGrowth will not be liable for any indirect or consequential loss or damage whatever (including without limitation loss of business, opportunity, data, profits) arising out of or in connection with the use of the Service. 46 | * ForGrowth makes no warranty that the functionality of the Service will be uninterrupted or error free, that defects will be corrected or that the Service or any server that makes it available are free of viruses or anything else which may be harmful or destructive. 47 | 48 | #### Termination of Service 49 | 50 | * The obligations and liabilities of the parties incurred prior to the termination date shall survive the termination of this agreement for all purposes. 51 | * These Terms of Service are effective unless and until terminated by either you or us. You may terminate these Terms of Service at any time by notifying us that you no longer wish to use our Services, or when you cease using our site. 52 | * If in our sole judgment you fail, or we suspect that you have failed, to comply with any term or provision of these Terms of Service, we also may terminate this agreement at any time without notice and you will remain liable for all amounts due up to and including the date of termination; and/or accordingly may deny you access to our Services (or any part thereof). 53 | 54 | #### Indemnity 55 | 56 | You agree to indemnify and hold ForGrowth and its employees and agents harmless from and against all liabilities, legal fees, damages, losses, costs and other expenses in relation to any claims or actions brought against ForGrowth arising out of any breach by you of these 57 | Terms and Conditions or other liabilities arising out of your use of the Service. 58 | 59 | #### Severability 60 | 61 | If the event that any of these Terms and Conditions should be determined to be invalid, illegal or unenforceable for any reason by any court of competent jurisdiction then such Term or Condition shall be severed and the remaining Terms and Conditions shall survive and remain in full force and effect and continue to be binding and enforceable. 62 | 63 | #### Waiver 64 | 65 | If you breach these Conditions of Use and we take no action, we will still be entitled to use our rights and remedies in any other situation where you breach these Conditions of Use. 66 | 67 | #### Governing Law 68 | 69 | These Terms and Conditions shall be governed by and construed in accordance with the law of the United States of America and you hereby submit to the exclusive jurisdiction of the United States courts. 70 | 71 | #### Contact 72 | 73 | If you have any questions about these Terms, please contact us at z-yun@fourz.cn -------------------------------------------------------------------------------- /src/extension.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import './assets/fonts/font.css' 4 | import Extension from './Extension.vue' 5 | 6 | createApp(Extension).mount('#app') 7 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.css' 3 | import './assets/fonts/font.css' 4 | import Phone from './components/Phone.vue' 5 | 6 | createApp(Phone).mount('#app') 7 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Phone On Web | Android in browser", 3 | "description": "Using android phone in web browser without root, not need software download.", 4 | "homepage_url": "http://browserlify.com/?from=webstore_", 5 | "version": "-", 6 | "manifest_version": 3, 7 | "background": { 8 | "service_worker": "background.js" 9 | }, 10 | "permissions": [ 11 | "storage" 12 | ], 13 | "icons": { 14 | "64": "icons/icon_64.png", 15 | "96": "icons/icon_64.png", 16 | "128": "icons/icon_64.png" 17 | }, 18 | "action": { 19 | "default_title": "Using android in browser" 20 | }, 21 | "web_accessible_resources": [ 22 | { 23 | "resources": [ 24 | "icons/*", 25 | "assets/*" 26 | ], 27 | "matches": [ 28 | "" 29 | ] 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const defaultTheme = require('tailwindcss/defaultTheme') 3 | 4 | module.exports = { 5 | content: [ 6 | "./extension.html", 7 | "./src/**/*.{vue,js,ts,jsx,tsx}", 8 | ], 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | sans: ['Nunito', ...defaultTheme.fontFamily.sans], 13 | }, 14 | }, 15 | }, 16 | plugins: [ 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { resolve } from 'path' 3 | import vue from '@vitejs/plugin-vue' 4 | import Markdown from './plugins/md-loader' 5 | import Binary from './plugins/binary-loader' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | define: { 10 | 'process.env': process.env 11 | }, 12 | build: { 13 | lib: { 14 | entry: resolve(__dirname, 'src/main.js'), 15 | name: 'andbrowser', 16 | // the proper extensions will be added 17 | fileName: 'andbrowser' 18 | }, 19 | }, 20 | plugins: [ 21 | vue(), 22 | Markdown(), 23 | Binary(), 24 | ], 25 | assetsInclude: ["**/*.md", "**/*.jar"], 26 | }) 27 | -------------------------------------------------------------------------------- /vite.extension.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { resolve } from 'path' 3 | import vue from '@vitejs/plugin-vue' 4 | import Markdown from './plugins/md-loader' 5 | import Binary from './plugins/binary-loader' 6 | import { viteStaticCopy } from 'vite-plugin-static-copy' 7 | import { version } from './package.json' 8 | // https://vitejs.dev/config/ 9 | 10 | export default defineConfig({ 11 | define: { 12 | 'process.env': process.env 13 | }, 14 | plugins: [ 15 | vue(), 16 | Markdown(), 17 | Binary(), 18 | viteStaticCopy({ 19 | targets: [ 20 | { src: 'src/app/background.js', dest: '.' }, 21 | { src: 'src/app/plausible.js', dest: '.' }, 22 | { src: 'src/assets/icon_64.png', dest: 'icons' }, 23 | { 24 | src: 'src/manifest.json', 25 | dest: '.', 26 | transform: (content, fname) => { 27 | var obj = JSON.parse(content) 28 | obj.version = version 29 | obj.homepage_url = obj.homepage_url + process.env.VITE_STORE 30 | return JSON.stringify(obj, null, 2) 31 | }, 32 | } 33 | ] 34 | }) 35 | ], 36 | assetsInclude: ["**/*.md", "**/*.jar"], 37 | build: { 38 | assetsInlineLimit: 100 * 1024, 39 | rollupOptions: { 40 | output: { 41 | entryFileNames: `assets/[name].js`, 42 | chunkFileNames: `assets/[name].js`, 43 | assetFileNames: `assets/[name].[ext]` 44 | } 45 | } 46 | } 47 | }) 48 | --------------------------------------------------------------------------------